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 0000000000..e1344e25a1 Binary files /dev/null and b/packages/zone.js/doc/error.png differ diff --git a/packages/zone.js/doc/error.puml b/packages/zone.js/doc/error.puml new file mode 100644 index 0000000000..f3db461865 --- /dev/null +++ b/packages/zone.js/doc/error.puml @@ -0,0 +1,9 @@ +@startuml +scheduling --> unknown: zoneSpec.onScheduleTask\nor task.scheduleFn\nthrow error +running --> scheduled: error in \ntask.callback\nand task is\nperiodical\ntask +running --> notScheduled: error in\ntask.callback\nand\ntask is not\nperiodical +running: zoneSpec.onHandleError +running --> throw: error in\n task.callback\n and \nzoneSpec.onHandleError\n return true +canceling --> unknown: zoneSpec.onCancelTask\n or task.cancelFn\n throw error +unknown --> throw +@enduml \ No newline at end of file diff --git a/packages/zone.js/doc/eventtask.png b/packages/zone.js/doc/eventtask.png new file mode 100644 index 0000000000..eb984aee38 Binary files /dev/null and b/packages/zone.js/doc/eventtask.png differ diff --git a/packages/zone.js/doc/eventtask.puml b/packages/zone.js/doc/eventtask.puml new file mode 100644 index 0000000000..6ba4fe3b2f --- /dev/null +++ b/packages/zone.js/doc/eventtask.puml @@ -0,0 +1,21 @@ +@startuml +[*] --> notScheduled: initialize +notScheduled --> scheduling: addEventListener + +scheduling: zoneSpec.onScheduleTask +scheduling: zoneSpec.onHasTask + +scheduling --> scheduled +scheduled --> running: event\n triggered +running: zoneSpec:onInvokeTask + +scheduled --> canceling: removeEventListener +canceling: zoneSpec.onCancelTask +canceling --> notScheduled +canceling: zoneSpec.onHasTask + +running --> scheduled: callback\n finished +running: zoneSpec.onHasTask +running --> canceling: removeEventListener + +@enduml \ No newline at end of file diff --git a/packages/zone.js/doc/microtask.png b/packages/zone.js/doc/microtask.png new file mode 100644 index 0000000000..330359490e Binary files /dev/null and b/packages/zone.js/doc/microtask.png differ diff --git a/packages/zone.js/doc/microtask.puml b/packages/zone.js/doc/microtask.puml new file mode 100644 index 0000000000..0529d65032 --- /dev/null +++ b/packages/zone.js/doc/microtask.puml @@ -0,0 +1,14 @@ +@startuml +[*] --> notScheduled: initialize +notScheduled --> scheduling: promise.then/\nprocess.nextTick\nand so on + +scheduling: zoneSpec.onScheduleTask +scheduling: zoneSpec.onHasTask + +scheduling --> scheduled +scheduled --> running: callback +running: zoneSpec:onInvokeTask + +running --> notScheduled +running: zoneSpec.onHasTask +@enduml \ No newline at end of file diff --git a/packages/zone.js/doc/non-periodical-macrotask.png b/packages/zone.js/doc/non-periodical-macrotask.png new file mode 100644 index 0000000000..2fdcc1127b Binary files /dev/null and b/packages/zone.js/doc/non-periodical-macrotask.png differ diff --git a/packages/zone.js/doc/non-periodical-macrotask.puml b/packages/zone.js/doc/non-periodical-macrotask.puml new file mode 100644 index 0000000000..a8d0456c45 --- /dev/null +++ b/packages/zone.js/doc/non-periodical-macrotask.puml @@ -0,0 +1,18 @@ +@startuml +[*] --> notScheduled: initialize +notScheduled --> scheduling: setTimeout/\nXMLHttpRequest.send\nand so on + +scheduling: zoneSpec.onScheduleTask +scheduling: zoneSpec.onHasTask + +scheduling --> scheduled +scheduled --> running: timeout callback\nreadystatechange\ncallback +running: zoneSpec:onInvokeTask + +scheduled --> canceling: clearTimeout\n/abort request +canceling: zoneSpec.onCancelTask +canceling --> notScheduled +canceling: zoneSpec.onHasTask +running --> notScheduled +running: zoneSpec.onHasTask +@enduml diff --git a/packages/zone.js/doc/override-task.png b/packages/zone.js/doc/override-task.png new file mode 100644 index 0000000000..20136425e2 Binary files /dev/null and b/packages/zone.js/doc/override-task.png differ diff --git a/packages/zone.js/doc/override-task.puml b/packages/zone.js/doc/override-task.puml new file mode 100644 index 0000000000..d0e586539c --- /dev/null +++ b/packages/zone.js/doc/override-task.puml @@ -0,0 +1,18 @@ +@startuml +[*] --> notScheduled: initialize +notScheduled --> scheduling: scheduleTask + +scheduling: zoneSpec.onScheduleTask +scheduling: zoneSpec.onHasTask + +scheduling --> scheduled: override with\n anotherZone +scheduled --> running: timeout callback\nreadystatechange\ncallback +running: anotherZoneSpec:onInvokeTask + +scheduled --> canceling: clearTimeout\n/abort request +canceling: anotherZoneSpec.onCancelTask +canceling --> notScheduled +canceling: zneSpec.onHasTask +running --> notScheduled +running: zoneSpec.onHasTask +@enduml \ No newline at end of file diff --git a/packages/zone.js/doc/periodical-macrotask.png b/packages/zone.js/doc/periodical-macrotask.png new file mode 100644 index 0000000000..e673d23ff3 Binary files /dev/null and b/packages/zone.js/doc/periodical-macrotask.png differ diff --git a/packages/zone.js/doc/periodical-macrotask.puml b/packages/zone.js/doc/periodical-macrotask.puml new file mode 100644 index 0000000000..905968df36 --- /dev/null +++ b/packages/zone.js/doc/periodical-macrotask.puml @@ -0,0 +1,18 @@ +@startuml +[*] --> notScheduled: initialize +notScheduled --> scheduling: setInterval + +scheduling: zoneSpec.onScheduleTask +scheduling: zoneSpec.onHasTask + +scheduling --> scheduled +scheduled --> running: interval\n callback +running: zoneSpec:onInvokeTask + +scheduled --> canceling: clearInterval +canceling: zoneSpec.onCancelTask +canceling --> notScheduled +canceling: zoneSpec.onHasTask +running --> scheduled: callback\n finished +running --> canceling: clearInterval +@enduml \ No newline at end of file diff --git a/packages/zone.js/doc/reschedule-task.png b/packages/zone.js/doc/reschedule-task.png new file mode 100644 index 0000000000..ba4cc71c6e Binary files /dev/null and b/packages/zone.js/doc/reschedule-task.png differ diff --git a/packages/zone.js/doc/reschedule-task.puml b/packages/zone.js/doc/reschedule-task.puml new file mode 100644 index 0000000000..49d99e0cbd --- /dev/null +++ b/packages/zone.js/doc/reschedule-task.puml @@ -0,0 +1,20 @@ +@startuml +[*] --> notScheduled: initialize +notScheduled --> scheduling: â‘ current zone\n scheduleTask +notScheduled --> scheduling: â‘¢anotherZone\n scheduleTask + +scheduling: anotherZoneSpec.onScheduleTask +scheduling: anotherZoneSpec.onHasTask + +scheduling --> notScheduled: â‘¡cancelScheduleRequest +scheduling --> scheduled +scheduled --> running: callback +running: anotherZoneSpec:onInvokeTask + +scheduled --> canceling: cancelTask +canceling: anotherZoneSpec.onCancelTask +canceling --> notScheduled +canceling: anotherZoneSpec.onHasTask +running --> notScheduled +running: anotherZoneSpec.onHasTask +@enduml \ No newline at end of file diff --git a/packages/zone.js/doc/task.md b/packages/zone.js/doc/task.md new file mode 100644 index 0000000000..4b08beb93d --- /dev/null +++ b/packages/zone.js/doc/task.md @@ -0,0 +1,80 @@ +## Task lifecycle + +We handle several kinds of tasks in zone.js, + +- MacroTask +- MicroTask +- EventTask + +For details, please refer to [here](../dist/zone.js.d.ts) + +This document will explain the lifecycle (state-transition) of different types of tasks and also the triggering of various zonespec's callback during that cycle. + +The motivation to write this document has come from this [PR](https://github.com/angular/zone.js/pull/629) of @mhevery. This has made the task's state more clear. Also, tasks can now be cancelled and rescheduled in different zone. + +### MicroTask +Such as Promise.then, process.nextTick, they are microTasks, the lifecycle(state transition) +looks like this. + +![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

+ +
    +
  1. Tracing user actions with long stack traces
  2. +
  3. Counting Tasks
  4. +
  5. Profiling Across Tasks
  6. +
  7. Throttle
  8. +
  9. WebSocket
  10. +
+ + + 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 0000000000..3952ce243a Binary files /dev/null and b/packages/zone.js/presentation.png differ diff --git a/packages/zone.js/promise-adapter.js b/packages/zone.js/promise-adapter.js new file mode 100644 index 0000000000..be9f9ac1eb --- /dev/null +++ b/packages/zone.js/promise-adapter.js @@ -0,0 +1,18 @@ +require('./build/lib/node/rollup-main'); +Zone[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = true; +module.exports.deferred = function() { + const p = {}; + p.promise = new Promise((resolve, reject) => { + p.resolve = resolve; + p.reject = reject; + }); + return p; +}; + +module.exports.resolved = (val) => { + return Promise.resolve(val); +}; + +module.exports.rejected = (reason) => { + return Promise.reject(reason); +}; diff --git a/packages/zone.js/promise-test.js b/packages/zone.js/promise-test.js new file mode 100644 index 0000000000..c9ab27ad36 --- /dev/null +++ b/packages/zone.js/promise-test.js @@ -0,0 +1,10 @@ +const promisesAplusTests = require('promises-aplus-tests'); +const adapter = require('./promise-adapter'); +promisesAplusTests(adapter, {reporter: 'dot'}, function(err) { + if (err) { + console.error(err); + process.exit(1); + } else { + process.exit(0); + } +}); diff --git a/packages/zone.js/promise.finally.spec.js b/packages/zone.js/promise.finally.spec.js new file mode 100644 index 0000000000..6695b2981b --- /dev/null +++ b/packages/zone.js/promise.finally.spec.js @@ -0,0 +1,358 @@ +'use strict'; + +var assert = require('assert'); +var adapter = require('./promise-adapter'); +var P = global[Zone.__symbol__('Promise')]; + +var someRejectionReason = {message: 'some rejection reason'}; +var anotherReason = {message: 'another rejection reason'}; +process.on( + 'unhandledRejection', function(reason, promise) { console.log('unhandledRejection', reason); }); + +describe('mocha promise sanity check', () => { + it('passes with a resolved promise', () => { return P.resolve(3); }); + + it('passes with a rejected then resolved promise', + () => { return P.reject(someRejectionReason).catch(x => 'this should be resolved'); }); + + var ifPromiseIt = P === Promise ? it : it.skip; + ifPromiseIt('is the native Promise', () => { assert.equal(P, Promise); }); +}); + +describe('onFinally', () => { + describe('no callback', () => { + specify('from resolved', (done) => { + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally() + .then( + function onFulfilled(x) { + assert.strictEqual(x, 3); + done(); + }, + function onRejected() { done(new Error('should not be called')); }); + }); + + specify('from rejected', (done) => { + adapter.rejected(someRejectionReason) + .catch((e) => { + assert.strictEqual(e, someRejectionReason); + throw e; + }) + .finally() + .then( + function onFulfilled() { done(new Error('should not be called')); }, + function onRejected(reason) { + assert.strictEqual(reason, someRejectionReason); + done(); + }); + }); + }); + + describe('throws an exception', () => { + specify('from resolved', (done) => { + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + throw someRejectionReason; + }) + .then( + function onFulfilled() { done(new Error('should not be called')); }, + function onRejected(reason) { + assert.strictEqual(reason, someRejectionReason); + done(); + }); + }); + + specify('from rejected', (done) => { + adapter.rejected(anotherReason) + .finally(function onFinally() { + assert(arguments.length === 0); + throw someRejectionReason; + }) + .then( + function onFulfilled() { done(new Error('should not be called')); }, + function onRejected(reason) { + assert.strictEqual(reason, someRejectionReason); + done(); + }); + }); + }); + + describe('returns a non-promise', () => { + specify('from resolved', (done) => { + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + return 4; + }) + .then( + function onFulfilled(x) { + assert.strictEqual(x, 3); + done(); + }, + function onRejected() { done(new Error('should not be called')); }); + }); + + specify('from rejected', (done) => { + adapter.rejected(anotherReason) + .catch((e) => { + assert.strictEqual(e, anotherReason); + throw e; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + throw someRejectionReason; + }) + .then( + function onFulfilled() { done(new Error('should not be called')); }, + function onRejected(e) { + assert.strictEqual(e, someRejectionReason); + done(); + }); + }); + }); + + describe('returns a pending-forever promise', () => { + specify('from resolved', (done) => { + var timeout; + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + timeout = setTimeout(done, 0.1e3); + return new P(() => {}); // forever pending + }) + .then( + function onFulfilled(x) { + clearTimeout(timeout); + done(new Error('should not be called')); + }, + function onRejected() { + clearTimeout(timeout); + done(new Error('should not be called')); + }); + }); + + specify('from rejected', (done) => { + var timeout; + adapter.rejected(someRejectionReason) + .catch((e) => { + assert.strictEqual(e, someRejectionReason); + throw e; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + timeout = setTimeout(done, 0.1e3); + return new P(() => {}); // forever pending + }) + .then( + function onFulfilled(x) { + clearTimeout(timeout); + done(new Error('should not be called')); + }, + function onRejected() { + clearTimeout(timeout); + done(new Error('should not be called')); + }); + }); + }); + + describe('returns an immediately-fulfilled promise', () => { + specify('from resolved', (done) => { + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + return adapter.resolved(4); + }) + .then( + function onFulfilled(x) { + assert.strictEqual(x, 3); + done(); + }, + function onRejected() { done(new Error('should not be called')); }); + }); + + specify('from rejected', (done) => { + adapter.rejected(someRejectionReason) + .catch((e) => { + assert.strictEqual(e, someRejectionReason); + throw e; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + return adapter.resolved(4); + }) + .then( + function onFulfilled() { done(new Error('should not be called')); }, + function onRejected(e) { + assert.strictEqual(e, someRejectionReason); + done(); + }); + }); + }); + + describe('returns an immediately-rejected promise', () => { + specify('from resolved ', (done) => { + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + return adapter.rejected(4); + }) + .then( + function onFulfilled(x) { done(new Error('should not be called')); }, + function onRejected(e) { + assert.strictEqual(e, 4); + done(); + }); + }); + + specify('from rejected', (done) => { + const newReason = {}; + adapter.rejected(someRejectionReason) + .catch((e) => { + assert.strictEqual(e, someRejectionReason); + throw e; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + return adapter.rejected(newReason); + }) + .then( + function onFulfilled(x) { done(new Error('should not be called')); }, + function onRejected(e) { + assert.strictEqual(e, newReason); + done(); + }); + }); + }); + + describe('returns a fulfilled-after-a-second promise', () => { + specify('from resolved', (done) => { + var timeout; + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + timeout = setTimeout(done, 1.5e3); + return new P((resolve) => { setTimeout(() => resolve(4), 1e3); }); + }) + .then( + function onFulfilled(x) { + clearTimeout(timeout); + assert.strictEqual(x, 3); + done(); + }, + function onRejected() { + clearTimeout(timeout); + done(new Error('should not be called')); + }); + }); + + specify('from rejected', (done) => { + var timeout; + adapter.rejected(3) + .catch((e) => { + assert.strictEqual(e, 3); + throw e; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + timeout = setTimeout(done, 1.5e3); + return new P((resolve) => { setTimeout(() => resolve(4), 1e3); }); + }) + .then( + function onFulfilled() { + clearTimeout(timeout); + done(new Error('should not be called')); + }, + function onRejected(e) { + clearTimeout(timeout); + assert.strictEqual(e, 3); + done(); + }); + }); + }); + + describe('returns a rejected-after-a-second promise', () => { + specify('from resolved', (done) => { + var timeout; + adapter.resolved(3) + .then((x) => { + assert.strictEqual(x, 3); + return x; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + timeout = setTimeout(done, 1.5e3); + return new P((resolve, reject) => { setTimeout(() => reject(4), 1e3); }); + }) + .then( + function onFulfilled() { + clearTimeout(timeout); + done(new Error('should not be called')); + }, + function onRejected(e) { + clearTimeout(timeout); + assert.strictEqual(e, 4); + done(); + }); + }); + + specify('from rejected', (done) => { + var timeout; + adapter.rejected(someRejectionReason) + .catch((e) => { + assert.strictEqual(e, someRejectionReason); + throw e; + }) + .finally(function onFinally() { + assert(arguments.length === 0); + timeout = setTimeout(done, 1.5e3); + return new P((resolve, reject) => { setTimeout(() => reject(anotherReason), 1e3); }); + }) + .then( + function onFulfilled() { + clearTimeout(timeout); + done(new Error('should not be called')); + }, + function onRejected(e) { + clearTimeout(timeout); + assert.strictEqual(e, anotherReason); + done(); + }); + }); + }); + + specify('has the correct property descriptor', () => { + var descriptor = Object.getOwnPropertyDescriptor(Promise.prototype, 'finally'); + + assert.strictEqual(descriptor.writable, true); + assert.strictEqual(descriptor.configurable, true); + }); +}); \ No newline at end of file diff --git a/packages/zone.js/sauce-evergreen.conf.js b/packages/zone.js/sauce-evergreen.conf.js new file mode 100644 index 0000000000..ce874ea63a --- /dev/null +++ b/packages/zone.js/sauce-evergreen.conf.js @@ -0,0 +1,66 @@ +// Sauce configuration + +module.exports = function(config, ignoredLaunchers) { + // The WS server is not available with Sauce + config.files.unshift('test/saucelabs.js'); + + var basicLaunchers = { + 'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '72'}, + 'SL_CHROME_60': {base: 'SauceLabs', browserName: 'chrome', version: '60'}, + 'SL_ANDROID8.0': { + base: 'SauceLabs', + browserName: 'Chrome', + appiumVersion: '1.9.1', + platformName: 'Android', + deviceName: 'Android GoogleAPI Emulator', + platformVersion: '8.0' + } + }; + + var customLaunchers = {}; + if (!ignoredLaunchers) { + customLaunchers = basicLaunchers; + } else { + Object.keys(basicLaunchers).forEach(function(key) { + if (ignoredLaunchers.filter(function(ignore) { return ignore === key; }).length === 0) { + customLaunchers[key] = basicLaunchers[key]; + } + }); + } + + config.set({ + captureTimeout: 120000, + browserNoActivityTimeout: 240000, + + sauceLabs: { + testName: 'Zone.js', + startConnect: false, + recordVideo: false, + recordScreenshots: false, + options: { + 'selenium-version': '3.4.0', + 'command-timeout': 600, + 'idle-timeout': 600, + 'max-duration': 5400 + } + }, + + customLaunchers: customLaunchers, + + browsers: Object.keys(customLaunchers), + + reporters: ['dots', 'saucelabs'], + + singleRun: true, + + plugins: ['karma-*'] + }); + + if (process.env.TRAVIS) { + config.sauceLabs.build = + 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; + config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + + process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); + } +}; diff --git a/packages/zone.js/sauce-selenium3.conf.js b/packages/zone.js/sauce-selenium3.conf.js new file mode 100644 index 0000000000..0141e88089 --- /dev/null +++ b/packages/zone.js/sauce-selenium3.conf.js @@ -0,0 +1,49 @@ +// Sauce configuration with Welenium drivers 3+ + +module.exports = function(config) { + // The WS server is not available with Sauce + config.files.unshift('test/saucelabs.js'); + + var customLaunchers = { + 'SL_CHROME60': + {base: 'SauceLabs', browserName: 'Chrome', platform: 'Windows 10', version: '60.0'}, + 'SL_SAFARI11': + {base: 'SauceLabs', browserName: 'safari', platform: 'macOS 10.13', version: '11.1'}, + }; + + config.set({ + captureTimeout: 120000, + browserNoActivityTimeout: 240000, + + sauceLabs: { + testName: 'Zone.js', + startConnect: false, + recordVideo: false, + recordScreenshots: false, + options: { + 'selenium-version': '3.5.0', + 'command-timeout': 600, + 'idle-timeout': 600, + 'max-duration': 5400 + } + }, + + customLaunchers: customLaunchers, + + browsers: Object.keys(customLaunchers), + + reporters: ['dots', 'saucelabs'], + + singleRun: true, + + plugins: ['karma-*'] + }); + + if (process.env.TRAVIS) { + config.sauceLabs.build = + 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; + config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + + process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); + } +}; diff --git a/packages/zone.js/sauce.conf.js b/packages/zone.js/sauce.conf.js new file mode 100644 index 0000000000..aa24eefa22 --- /dev/null +++ b/packages/zone.js/sauce.conf.js @@ -0,0 +1,151 @@ +// Sauce configuration + +module.exports = function(config, ignoredLaunchers) { + // The WS server is not available with Sauce + config.files.unshift('test/saucelabs.js'); + + var basicLaunchers = { + 'SL_CHROME': {base: 'SauceLabs', browserName: 'chrome', version: '48'}, + 'SL_CHROME_65': {base: 'SauceLabs', browserName: 'chrome', version: '60'}, + 'SL_FIREFOX': {base: 'SauceLabs', browserName: 'firefox', version: '52'}, + 'SL_FIREFOX_59': {base: 'SauceLabs', browserName: 'firefox', version: '54'}, + /*'SL_SAFARI7': { + base: 'SauceLabs', + browserName: 'safari', + platform: 'OS X 10.9', + version: '7.0' + },*/ + //'SL_SAFARI8': + // {base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.10', version: '8.0'}, + 'SL_SAFARI9': + {base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.11', version: '9.0'}, + 'SL_SAFARI10': + {base: 'SauceLabs', browserName: 'safari', platform: 'OS X 10.11', version: '10.0'}, + /* + no longer supported in SauceLabs + 'SL_IOS7': { + base: 'SauceLabs', + browserName: 'iphone', + platform: 'OS X 10.10', + version: '7.1' + },*/ + /*'SL_IOS8': { + base: 'SauceLabs', + browserName: 'iphone', + platform: 'OS X 10.10', + version: '8.4' + },*/ + // 'SL_IOS9': {base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: + // '9.3'}, + 'SL_IOS10': {base: 'SauceLabs', browserName: 'iphone', platform: 'OS X 10.10', version: '10.3'}, + 'SL_IE9': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 2008', + version: '9' + }, + 'SL_IE10': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 2012', + version: '10' + }, + 'SL_IE11': { + base: 'SauceLabs', + browserName: 'internet explorer', + platform: 'Windows 10', + version: '11' + }, + 'SL_MSEDGE': { + base: 'SauceLabs', + browserName: 'MicrosoftEdge', + platform: 'Windows 10', + version: '14.14393' + }, + 'SL_MSEDGE15': { + base: 'SauceLabs', + browserName: 'MicrosoftEdge', + platform: 'Windows 10', + version: '15.15063' + }, + /* + fix issue #584, Android 4.1~4.3 are not supported + 'SL_ANDROID4.1': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '4.1' + }, + 'SL_ANDROID4.2': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '4.2' + }, + 'SL_ANDROID4.3': { + base: 'SauceLabs', + browserName: 'android', + platform: 'Linux', + version: '4.3' + },*/ + // 'SL_ANDROID4.4': {base: 'SauceLabs', browserName: 'android', platform: 'Linux', version: + // '4.4'}, + 'SL_ANDROID5.1': {base: 'SauceLabs', browserName: 'android', platform: 'Linux', version: '5.1'}, + 'SL_ANDROID6.0': {base: 'SauceLabs', browserName: 'android', platform: 'Linux', version: '6.0'}, + 'SL_ANDROID8.0': { + base: 'SauceLabs', + browserName: 'Chrome', + appiumVersion: '1.12.1', + platformName: 'Android', + deviceName: 'Android GoogleAPI Emulator', + platformVersion: '8.0' + } + }; + + var customLaunchers = {}; + if (!ignoredLaunchers) { + customLaunchers = basicLaunchers; + } else { + Object.keys(basicLaunchers).forEach(function(key) { + if (ignoredLaunchers.filter(function(ignore) { return ignore === key; }).length === 0) { + customLaunchers[key] = basicLaunchers[key]; + } + }); + } + + config.set({ + captureTimeout: 120000, + browserNoActivityTimeout: 240000, + + sauceLabs: { + testName: 'Zone.js', + startConnect: false, + recordVideo: false, + recordScreenshots: false, + options: { + 'selenium-version': '2.53.0', + 'command-timeout': 600, + 'idle-timeout': 600, + 'max-duration': 5400 + } + }, + + customLaunchers: customLaunchers, + + browsers: Object.keys(customLaunchers), + + reporters: ['dots', 'saucelabs'], + + singleRun: true, + + plugins: ['karma-*'] + }); + + if (process.env.TRAVIS) { + config.sauceLabs.build = + 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; + config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + + process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); + } +}; diff --git a/packages/zone.js/sauce.es2015.conf.js b/packages/zone.js/sauce.es2015.conf.js new file mode 100644 index 0000000000..cea30d38d1 --- /dev/null +++ b/packages/zone.js/sauce.es2015.conf.js @@ -0,0 +1,57 @@ +// Sauce configuration + +module.exports = function(config, ignoredLaunchers) { + // The WS server is not available with Sauce + config.files.unshift('test/saucelabs.js'); + + var basicLaunchers = { + 'SL_CHROME_66': {base: 'SauceLabs', browserName: 'chrome', version: '66'}, + }; + + var customLaunchers = {}; + if (!ignoredLaunchers) { + customLaunchers = basicLaunchers; + } else { + Object.keys(basicLaunchers).forEach(function(key) { + if (ignoredLaunchers.filter(function(ignore) { return ignore === key; }).length === 0) { + customLaunchers[key] = basicLaunchers[key]; + } + }); + } + + config.set({ + captureTimeout: 120000, + browserNoActivityTimeout: 240000, + + sauceLabs: { + testName: 'Zone.js', + startConnect: false, + recordVideo: false, + recordScreenshots: false, + options: { + 'selenium-version': '2.53.0', + 'command-timeout': 600, + 'idle-timeout': 600, + 'max-duration': 5400 + } + }, + + customLaunchers: customLaunchers, + + browsers: Object.keys(customLaunchers), + + reporters: ['dots', 'saucelabs'], + + singleRun: true, + + plugins: ['karma-*'] + }); + + if (process.env.TRAVIS) { + config.sauceLabs.build = + 'TRAVIS #' + process.env.TRAVIS_BUILD_NUMBER + ' (' + process.env.TRAVIS_BUILD_ID + ')'; + config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER; + + process.env.SAUCE_ACCESS_KEY = process.env.SAUCE_ACCESS_KEY.split('').reverse().join(''); + } +}; diff --git a/packages/zone.js/scripts/closure/closure_compiler.sh b/packages/zone.js/scripts/closure/closure_compiler.sh new file mode 100755 index 0000000000..638ffcf108 --- /dev/null +++ b/packages/zone.js/scripts/closure/closure_compiler.sh @@ -0,0 +1,31 @@ +# compile closure test source file +$(npm bin)/tsc -p . +# Run the Google Closure compiler java runnable with zone externs +java -jar node_modules/google-closure-compiler/compiler.jar --flagfile 'scripts/closure/closure_flagfile' --externs 'lib/closure/zone_externs.js' + +# the names of Zone exposed API should be kept correctly with zone externs, test program should exit with 0. +node build/closure/closure-bundle.js + +if [ $? -eq 0 ] +then + echo "Successfully pass closure compiler with zone externs" +else + echo "failed to pass closure compiler with zone externs" + exit 1 +fi + +# Run the Google Closure compiler java runnable without zone externs. +java -jar node_modules/google-closure-compiler/compiler.jar --flagfile 'scripts/closure/closure_flagfile' + +# the names of Zone exposed API should be renamed and fail to be executed, test program should exit with 1. +node build/closure/closure-bundle.js + +if [ $? -eq 1 ] +then + echo "Successfully detect closure compiler error without zone externs" +else + echo "failed to detect closure compiler error without zone externs" + exit 1 +fi + +exit 0 diff --git a/packages/zone.js/scripts/closure/closure_flagfile b/packages/zone.js/scripts/closure/closure_flagfile new file mode 100644 index 0000000000..524aa0ef66 --- /dev/null +++ b/packages/zone.js/scripts/closure/closure_flagfile @@ -0,0 +1,5 @@ +--compilation_level ADVANCED_OPTIMIZATIONS +--js_output_file "build/closure/closure-bundle.js" +--rewrite_polyfills false +--js "build/test/closure/zone.closure.js" +--formatting PRETTY_PRINT \ No newline at end of file diff --git a/packages/zone.js/scripts/grab-blink-idl.sh b/packages/zone.js/scripts/grab-blink-idl.sh new file mode 100755 index 0000000000..4884ea73d8 --- /dev/null +++ b/packages/zone.js/scripts/grab-blink-idl.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +set -e + +trap "echo Exit; exit;" SIGINT SIGTERM + +CORE_URL="https://src.chromium.org/blink/trunk/Source/core/" +MODULE_URL="https://src.chromium.org/blink/trunk/Source/modules/" + +mkdir -p blink-idl/core +mkdir -p blink-idl/modules + + +echo "Fetching core idl files..." + +rm tmp/ -rf +svn co $CORE_URL tmp -q + +for IDL in $(find tmp/ -iname '*.idl' -type f -printf '%P\n') +do + echo "- $IDL" + mv "tmp/$IDL" blink-idl/core +done + +echo "Fetching modules idl files..." + +rm tmp/ -rf +svn co $MODULE_URL tmp -q + +for IDL in $(find tmp/ -iname '*.idl' -type f -printf '%P\n') +do + echo "- $IDL" + mv "tmp/$IDL" blink-idl/modules +done + +rm tmp/ -rf diff --git a/packages/zone.js/scripts/sauce/sauce_connect_block.sh b/packages/zone.js/scripts/sauce/sauce_connect_block.sh new file mode 100755 index 0000000000..ebda1fccb0 --- /dev/null +++ b/packages/zone.js/scripts/sauce/sauce_connect_block.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Wait for Connect to be ready before exiting +printf "Connecting to Sauce." +while [ ! -f $BROWSER_PROVIDER_READY_FILE ]; do + printf "." + #dart2js takes longer than the travis 10 min timeout to complete + sleep .5 +done +echo "Connected" \ No newline at end of file diff --git a/packages/zone.js/scripts/sauce/sauce_connect_setup.sh b/packages/zone.js/scripts/sauce/sauce_connect_setup.sh new file mode 100755 index 0000000000..5a88eaa53f --- /dev/null +++ b/packages/zone.js/scripts/sauce/sauce_connect_setup.sh @@ -0,0 +1,49 @@ +#!/bin/bash + +set -e -o pipefail + +# Setup and start Sauce Connect for your TravisCI build +# This script requires your .travis.yml to include the following two private env variables: +# SAUCE_USERNAME +# SAUCE_ACCESS_KEY +# Follow the steps at https://saucelabs.com/opensource/travis to set that up. +# +# Curl and run this script as part of your .travis.yml before_script section: +# before_script: +# - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash + +CONNECT_URL="https://saucelabs.com/downloads/sc-4.3.14-linux.tar.gz" +CONNECT_DIR="/tmp/sauce-connect-$RANDOM" +CONNECT_DOWNLOAD="sc-latest-linux.tar.gz" + +CONNECT_LOG="$LOGS_DIR/sauce-connect" +CONNECT_STDOUT="$LOGS_DIR/sauce-connect.stdout" +CONNECT_STDERR="$LOGS_DIR/sauce-connect.stderr" + +# Get Connect and start it +mkdir -p $CONNECT_DIR +cd $CONNECT_DIR +curl $CONNECT_URL -o $CONNECT_DOWNLOAD 2> /dev/null 1> /dev/null +mkdir sauce-connect +tar --extract --file=$CONNECT_DOWNLOAD --strip-components=1 --directory=sauce-connect > /dev/null +rm $CONNECT_DOWNLOAD + +SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev` + +ARGS="" + +# Set tunnel-id only on Travis, to make local testing easier. +if [ ! -z "$TRAVIS_JOB_NUMBER" ]; then + ARGS="$ARGS --tunnel-identifier $TRAVIS_JOB_NUMBER" +fi +if [ ! -z "$BROWSER_PROVIDER_READY_FILE" ]; then + ARGS="$ARGS --readyfile $BROWSER_PROVIDER_READY_FILE" +fi + + +echo "Starting Sauce Connect in the background, logging into:" +echo " $CONNECT_LOG" +echo " $CONNECT_STDOUT" +echo " $CONNECT_STDERR" +sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY $ARGS \ + --reconnect 100 --no-ssl-bump-domains all --logfile $CONNECT_LOG 2> $CONNECT_STDERR 1> $CONNECT_STDOUT & diff --git a/packages/zone.js/simple-server.js b/packages/zone.js/simple-server.js new file mode 100644 index 0000000000..525883b2ef --- /dev/null +++ b/packages/zone.js/simple-server.js @@ -0,0 +1,34 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const http = require('http'); +const path = require('path'); +const fs = require('fs'); +let server; + +const localFolder = __dirname; + +function requestHandler(req, res) { + if (req.url === '/close') { + res.end('server closing'); + setTimeout(() => { process.exit(0); }, 1000); + } else { + const file = localFolder + req.url; + + fs.readFile(file, function(err, contents) { + if (!err) { + res.end(contents); + } else { + res.writeHead(404, {'Content-Type': 'text/html'}); + res.end('

404, Not Found!

'); + }; + }); + }; +}; + +server = http.createServer(requestHandler).listen(8080); \ No newline at end of file diff --git a/packages/zone.js/test/BUILD.bazel b/packages/zone.js/test/BUILD.bazel new file mode 100644 index 0000000000..f8f4b93cd5 --- /dev/null +++ b/packages/zone.js/test/BUILD.bazel @@ -0,0 +1,376 @@ +load("//tools:defaults.bzl", "jasmine_node_test", "ts_library") +load("@build_bazel_rules_nodejs//:defs.bzl", "rollup_bundle") +load("@npm_bazel_karma//:index.bzl", "karma_web_test", "karma_web_test_suite") +load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary") + +exports_files([ + "assets/sample.json", + "assets/worker.js", + "assets/import.html", +]) + +ts_library( + name = "common_spec_env", + testonly = True, + srcs = glob([ + "test_fake_polyfill.ts", + "wtf_mock.ts", + "test-env-setup-jasmine.ts", + ]), + deps = [ + "//packages/zone.js/lib", + ], +) + +ts_library( + name = "common_spec_srcs", + testonly = True, + srcs = glob( + [ + "common/*.ts", + "zone-spec/*.ts", + "rxjs/*.ts", + ], + exclude = [ + "common/Error.spec.ts", + ], + ), + deps = [ + ":common_spec_util", + "//packages/zone.js/lib", + "@npm//rxjs", + ], +) + +ts_library( + name = "common_spec_util", + testonly = True, + srcs = glob([ + "test-util.ts", + ]), + deps = [ + "//packages/zone.js/lib", + ], +) + +ts_library( + name = "error_spec_srcs", + testonly = True, + srcs = glob([ + "common/Error.spec.ts", + ]), + deps = [ + ":common_spec_util", + "//packages/zone.js/lib", + ], +) + +ts_library( + name = "test_node_lib", + testonly = True, + srcs = glob( + [ + "node/*.ts", + "node-env-setup.ts", + "node_entry_point_common.ts", + "node_entry_point.ts", + "node_entry_point_no_patch_clock.ts", + "test-env-setup-jasmine-no-patch-clock.ts", + ], + ), + deps = [ + ":common_spec_env", + ":common_spec_srcs", + ":common_spec_util", + "//packages/zone.js/lib", + "@npm//@types/shelljs", + "@npm//@types/systemjs", + "@npm//rxjs", + "@npm//shelljs", + "@npm//systemjs", + ], +) + +ts_library( + name = "bluebird_spec", + testonly = True, + srcs = glob([ + "extra/bluebird.spec.ts", + "node_bluebird_entry_point.ts", + ]), + deps = [ + ":common_spec_env", + "//packages/zone.js/lib", + "@npm//bluebird", + ], +) + +ts_library( + name = "error_spec", + testonly = True, + srcs = glob([ + "node_error_entry_point.ts", + "node_error_disable_policy_entry_point.ts", + "node_error_lazy_policy_entry_point.ts", + ]), + deps = [ + ":common_spec_env", + ":common_spec_util", + ":error_spec_srcs", + "//packages/zone.js/lib", + ], +) + +jasmine_node_test( + name = "test_node", + bootstrap = [ + "angular/packages/zone.js/test/node_entry_point.js", + ], + deps = [ + ":test_node_lib", + ], +) + +jasmine_node_test( + name = "test_node_no_jasmine_clock", + bootstrap = [ + "angular/packages/zone.js/test/node_entry_point_no_patch_clock.js", + ], + deps = [ + ":test_node_lib", + ], +) + +jasmine_node_test( + name = "test_node_bluebird", + bootstrap = [ + "angular/packages/zone.js/test/node_bluebird_entry_point.js", + ], + deps = [ + ":bluebird_spec", + ], +) + +jasmine_node_test( + name = "test_node_error_disable_policy", + bootstrap = [ + "angular/packages/zone.js/test/node_error_disable_policy_entry_point.js", + ], + deps = [ + ":error_spec", + ], +) + +jasmine_node_test( + name = "test_node_error_lazy_policy", + bootstrap = [ + "angular/packages/zone.js/test/node_error_lazy_policy_entry_point.js", + ], + deps = [ + ":error_spec", + ], +) + +ts_library( + name = "npm_package_spec_lib", + testonly = True, + srcs = ["npm_package/npm_package.spec.ts"], + deps = [ + "@npm//@types", + ], +) + +jasmine_node_test( + name = "test_npm_package", + srcs = [":npm_package_spec_lib"], + data = [ + "//packages/zone.js:npm_package", + "@npm//shelljs", + ], +) + +ts_library( + name = "test_browser_lib", + testonly = True, + srcs = glob( + [ + "browser/*.ts", + "extra/cordova.spec.ts", + "mocha-patch.spec.ts", + "jasmine-patch.spec.ts", + "common_tests.ts", + "browser_entry_point.ts", + ], + ), + deps = [ + ":common_spec_env", + ":common_spec_srcs", + ":common_spec_util", + ":error_spec_srcs", + "//packages/zone.js/lib", + "@npm//@types/shelljs", + "@npm//@types/systemjs", + "@npm//rxjs", + "@npm//shelljs", + "@npm//systemjs", + ], +) + +ts_library( + name = "browser_env_setup", + testonly = True, + srcs = glob([ + "browser-env-setup.ts", + "browser_symbol_setup.ts", + ]), + deps = [ + ":common_spec_env", + ], +) + +rollup_bundle( + name = "browser_test_env_setup_rollup", + testonly = True, + entry_point = ":browser-env-setup.ts", + deps = [ + ":browser_env_setup", + ], +) + +filegroup( + name = "browser_test_env_setup_rollup.es5", + testonly = True, + srcs = [":browser_test_env_setup_rollup"], + output_group = "umd", +) + +rollup_bundle( + name = "browser_test_rollup", + testonly = True, + entry_point = ":browser_entry_point.ts", + globals = { + "electron": "electron", + }, + deps = [ + ":test_browser_lib", + ], +) + +filegroup( + name = "browser_test_rollup.es5", + testonly = True, + srcs = [":browser_test_rollup"], + output_group = "umd", +) + +genrule( + name = "browser_test_trim_map", + testonly = True, + srcs = [ + ":browser_test_rollup.es5", + ], + outs = [ + "browser_test_rollup_trim_map.js", + ], + cmd = " && ".join([ + "cp $(@D)/browser_test_rollup.umd.js $@", + ]), +) + +genrule( + name = "browser_test_env_setup_trim_map", + testonly = True, + srcs = [ + ":browser_test_env_setup_rollup.es5", + ], + outs = [ + "browser_test_env_setup_rollup_trim_map.js", + ], + cmd = " && ".join([ + "cp $(@D)/browser_test_env_setup_rollup.umd.js $@", + ]), +) + +_karma_test_required_dist_files = [ + "//packages/zone.js/dist:task-tracking-dist-dev-test", + "//packages/zone.js/dist:wtf-dist-dev-test", + "//packages/zone.js/dist:webapis-notification-dist-dev-test", + "//packages/zone.js/dist:webapis-media-query-dist-dev-test", + "//packages/zone.js/dist:zone-patch-canvas-dist-dev-test", + "//packages/zone.js/dist:zone-patch-fetch-dist-dev-test", + "//packages/zone.js/dist:zone-patch-resize-observer-dist-dev-test", + "//packages/zone.js/dist:zone-patch-user-media-dist-dev-test", + ":browser_test_trim_map", +] + +karma_web_test_suite( + name = "karma_jasmine_test", + srcs = [ + "fake_entry.js", + ], + bootstrap = [ + ":browser_test_env_setup_trim_map", + "//packages/zone.js/dist:zone-testing-bundle-dist-dev-test", + ] + _karma_test_required_dist_files, + static_files = [ + ":assets/sample.json", + ":assets/worker.js", + ":assets/import.html", + ], + tags = ["zone_karma_test"], + runtime_deps = [ + "@npm//karma-browserstack-launcher", + ], +) + +karma_web_test_suite( + name = "karma_jasmine_evergreen_test", + srcs = [ + "fake_entry.js", + ], + bootstrap = [ + ":browser_test_env_setup_trim_map", + "//packages/zone.js/dist:zone-evergreen-dist-dev-test", + "//packages/zone.js/dist:zone-testing-dist-dev-test", + ] + _karma_test_required_dist_files, + data = [ + "//:browser-providers.conf.js", + "//tools:jasmine-seed-generator.js", + ], + static_files = [ + ":assets/sample.json", + ":assets/worker.js", + ":assets/import.html", + ], + tags = ["zone_karma_test"], + runtime_deps = [ + "@npm//karma-browserstack-launcher", + ], +) + +karma_web_test_suite( + name = "karma_jasmine_test_ci", + srcs = [ + "fake_entry.js", + ], + bootstrap = [ + ":saucelabs.js", + ":browser_test_env_setup_trim_map", + "//packages/zone.js/dist:zone-testing-bundle-dist-test", + ] + _karma_test_required_dist_files, + config_file = "//:karma-js.conf.js", + configuration_env_vars = ["KARMA_WEB_TEST_MODE"], + data = [ + "//:browser-providers.conf.js", + "//tools:jasmine-seed-generator.js", + ], + static_files = [ + ":assets/sample.json", + ":assets/worker.js", + ":assets/import.html", + ], + tags = ["zone_karma_test"], + runtime_deps = [ + "@npm//karma-browserstack-launcher", + ], +) diff --git a/packages/zone.js/test/assets/import.html b/packages/zone.js/test/assets/import.html new file mode 100644 index 0000000000..28f5df8830 --- /dev/null +++ b/packages/zone.js/test/assets/import.html @@ -0,0 +1 @@ +

hey

diff --git a/packages/zone.js/test/assets/sample.json b/packages/zone.js/test/assets/sample.json new file mode 100644 index 0000000000..56c8e28033 --- /dev/null +++ b/packages/zone.js/test/assets/sample.json @@ -0,0 +1 @@ +{"hello": "world"} diff --git a/packages/zone.js/test/assets/worker.js b/packages/zone.js/test/assets/worker.js new file mode 100644 index 0000000000..5c7a58f5b1 --- /dev/null +++ b/packages/zone.js/test/assets/worker.js @@ -0,0 +1,8 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +postMessage('worker'); \ No newline at end of file diff --git a/packages/zone.js/test/browser-env-setup.ts b/packages/zone.js/test/browser-env-setup.ts new file mode 100644 index 0000000000..76d3e66f93 --- /dev/null +++ b/packages/zone.js/test/browser-env-setup.ts @@ -0,0 +1,6 @@ +/// + +import './browser_symbol_setup'; +import './test_fake_polyfill'; +import './wtf_mock'; +import './test-env-setup-jasmine'; diff --git a/packages/zone.js/test/browser-zone-setup.ts b/packages/zone.js/test/browser-zone-setup.ts new file mode 100644 index 0000000000..3b3f4a809e --- /dev/null +++ b/packages/zone.js/test/browser-zone-setup.ts @@ -0,0 +1,29 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +if (typeof window !== 'undefined') { + const zoneSymbol = (window as any).Zone.__symbol__; + (window as any)['__Zone_enable_cross_context_check'] = true; + (window as any)[zoneSymbol('fakeAsyncAutoFakeAsyncWhenClockPatched')] = true; +} +import '../lib/common/to-string'; +import '../lib/browser/api-util'; +import '../lib/browser/browser-legacy'; +import '../lib/browser/browser'; +import '../lib/browser/canvas'; +import '../lib/common/fetch'; +import '../lib/browser/webapis-user-media'; +import '../lib/browser/webapis-media-query'; +import '../lib/testing/zone-testing'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/extra/cordova'; +import '../lib/testing/promise-testing'; +import '../lib/testing/async-testing'; +import '../lib/testing/fake-async'; +import '../lib/browser/webapis-resize-observer'; +import '../lib/rxjs/rxjs-fake-async'; diff --git a/packages/zone.js/test/browser/FileReader.spec.ts b/packages/zone.js/test/browser/FileReader.spec.ts new file mode 100644 index 0000000000..b84cd8dcdf --- /dev/null +++ b/packages/zone.js/test/browser/FileReader.spec.ts @@ -0,0 +1,106 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from '../test-util'; +declare const global: any; + +describe('FileReader', ifEnvSupports('FileReader', function() { + let fileReader: FileReader; + let blob: Blob; + const data = 'Hello, World!'; + const testZone = Zone.current.fork({name: 'TestZone'}); + + // Android 4.3's native browser doesn't implement add/RemoveEventListener for FileReader + function supportsEventTargetFns() { + return FileReader.prototype.addEventListener && + FileReader.prototype.removeEventListener; + } + (supportsEventTargetFns).message = + 'FileReader#addEventListener and FileReader#removeEventListener'; + + beforeEach(function() { + fileReader = new FileReader(); + + try { + blob = new Blob([data]); + } catch (e) { + // For hosts that don't support the Blob ctor (e.g. Android 4.3's native browser) + const blobBuilder = new global['WebKitBlobBuilder'](); + blobBuilder.append(data); + + blob = blobBuilder.getBlob(); + } + }); + + describe('EventTarget methods', ifEnvSupports(supportsEventTargetFns, function() { + it('should bind addEventListener listeners', function(done) { + testZone.run(function() { + fileReader.addEventListener('load', function() { + expect(Zone.current).toBe(testZone); + expect(fileReader.result).toEqual(data); + done(); + }); + }); + + fileReader.readAsText(blob); + }); + + it('should remove listeners via removeEventListener', function(done) { + const listenerSpy = jasmine.createSpy('listener'); + + testZone.run(function() { + fileReader.addEventListener('loadstart', listenerSpy); + fileReader.addEventListener('loadend', function() { + expect(listenerSpy).not.toHaveBeenCalled(); + done(); + }); + }); + + fileReader.removeEventListener('loadstart', listenerSpy); + fileReader.readAsText(blob); + }); + })); + + it('should bind onEventType listeners', function(done) { + let listenersCalled = 0; + + testZone.run(function() { + fileReader.onloadstart = function() { + listenersCalled++; + expect(Zone.current).toBe(testZone); + }; + + fileReader.onload = function() { + listenersCalled++; + expect(Zone.current).toBe(testZone); + }; + + fileReader.onloadend = function() { + listenersCalled++; + + expect(Zone.current).toBe(testZone); + expect(fileReader.result).toEqual(data); + expect(listenersCalled).toBe(3); + done(); + }; + }); + + fileReader.readAsText(blob); + }); + + it('should have correct readyState', function(done) { + fileReader.onloadend = function() { + expect(fileReader.readyState).toBe((FileReader).DONE); + done(); + }; + + expect(fileReader.readyState).toBe((FileReader).EMPTY); + + fileReader.readAsText(blob); + }); + })); \ No newline at end of file diff --git a/packages/zone.js/test/browser/HTMLImports.spec.ts b/packages/zone.js/test/browser/HTMLImports.spec.ts new file mode 100644 index 0000000000..ef271ebc5d --- /dev/null +++ b/packages/zone.js/test/browser/HTMLImports.spec.ts @@ -0,0 +1,79 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from '../test-util'; + +function supportsImports() { + return 'import' in document.createElement('link'); +} +(supportsImports).message = 'HTML Imports'; + +describe('HTML Imports', ifEnvSupports(supportsImports, function() { + const testZone = Zone.current.fork({name: 'test'}); + + it('should work with addEventListener', function(done) { + let link: HTMLLinkElement; + + testZone.run(function() { + link = document.createElement('link'); + link.rel = 'import'; + link.href = 'someUrl'; + link.addEventListener('error', function() { + expect(Zone.current).toBe(testZone); + document.head.removeChild(link); + done(); + }); + }); + + document.head.appendChild(link !); + }); + + function supportsOnEvents() { + const link = document.createElement('link'); + const linkPropDesc = Object.getOwnPropertyDescriptor(link, 'onerror'); + return !(linkPropDesc && linkPropDesc.value === null); + } + (supportsOnEvents).message = 'Supports HTMLLinkElement#onxxx patching'; + + + ifEnvSupports(supportsOnEvents, function() { + it('should work with onerror', function(done) { + let link: HTMLLinkElement; + + testZone.run(function() { + link = document.createElement('link'); + link.rel = 'import'; + link.href = 'anotherUrl'; + link.onerror = function() { + expect(Zone.current).toBe(testZone); + document.head.removeChild(link); + done(); + }; + }); + + document.head.appendChild(link !); + }); + + it('should work with onload', function(done) { + let link: HTMLLinkElement; + + testZone.run(function() { + link = document.createElement('link'); + link.rel = 'import'; + link.href = '/base/angular/packages/zone.js/test/assets/import.html'; + link.onload = function() { + expect(Zone.current).toBe(testZone); + document.head.removeChild(link); + done(); + }; + }); + + document.head.appendChild(link !); + }); + }); + })); diff --git a/packages/zone.js/test/browser/MediaQuery.spec.ts b/packages/zone.js/test/browser/MediaQuery.spec.ts new file mode 100644 index 0000000000..fcb6dfb39a --- /dev/null +++ b/packages/zone.js/test/browser/MediaQuery.spec.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {zoneSymbol} from '../../lib/common/utils'; +import {ifEnvSupports} from '../test-util'; +declare const global: any; + +function supportMediaQuery() { + const _global = + typeof window === 'object' && window || typeof self === 'object' && self || global; + return _global['MediaQueryList'] && _global['matchMedia']; +} + +describe('test mediaQuery patch', ifEnvSupports(supportMediaQuery, () => { + it('test whether addListener is patched', () => { + const mqList = window.matchMedia('min-width:500px'); + if (mqList && mqList['addListener']) { + expect((mqList as any)[zoneSymbol('addListener')]).toBeTruthy(); + } + }); + })); diff --git a/packages/zone.js/test/browser/MutationObserver.spec.ts b/packages/zone.js/test/browser/MutationObserver.spec.ts new file mode 100644 index 0000000000..2bbbb4abb0 --- /dev/null +++ b/packages/zone.js/test/browser/MutationObserver.spec.ts @@ -0,0 +1,67 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from '../test-util'; +declare const global: any; + + +describe('MutationObserver', ifEnvSupports('MutationObserver', function() { + let elt: HTMLDivElement; + const testZone = Zone.current.fork({name: 'test'}); + + beforeEach(function() { elt = document.createElement('div'); }); + + it('should run observers within the zone', function(done) { + let ob; + + testZone.run(function() { + ob = new MutationObserver(function() { + expect(Zone.current).toBe(testZone); + done(); + }); + + ob.observe(elt, {childList: true}); + }); + + elt.innerHTML = '

hey

'; + }); + + it('should only dequeue upon disconnect if something is observed', function() { + let ob: MutationObserver; + let flag = false; + const elt = document.createElement('div'); + const childZone = + Zone.current.fork({name: 'test', onInvokeTask: function() { flag = true; }}); + + childZone.run(function() { ob = new MutationObserver(function() {}); }); + + ob !.disconnect(); + expect(flag).toBe(false); + }); + })); + +describe('WebKitMutationObserver', ifEnvSupports('WebKitMutationObserver', function() { + const testZone = Zone.current.fork({name: 'test'}); + + it('should run observers within the zone', function(done) { + let elt: HTMLDivElement; + + testZone.run(function() { + elt = document.createElement('div'); + + const ob = new global['WebKitMutationObserver'](function() { + expect(Zone.current).toBe(testZone); + done(); + }); + + ob.observe(elt, {childList: true}); + }); + + elt !.innerHTML = '

hey

'; + }); + })); diff --git a/packages/zone.js/test/browser/Notification.spec.ts b/packages/zone.js/test/browser/Notification.spec.ts new file mode 100644 index 0000000000..8e02739913 --- /dev/null +++ b/packages/zone.js/test/browser/Notification.spec.ts @@ -0,0 +1,26 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {zoneSymbol} from '../../lib/common/utils'; +import {ifEnvSupports} from '../test-util'; +declare const window: any; + +function notificationSupport() { + const desc = window['Notification'] && + Object.getOwnPropertyDescriptor(window['Notification'].prototype, 'onerror'); + return window['Notification'] && window['Notification'].prototype && desc && desc.configurable; +} + +(notificationSupport).message = 'Notification Support'; + +describe('Notification API', ifEnvSupports(notificationSupport, function() { + it('Notification API should be patched by Zone', () => { + const Notification = window['Notification']; + expect(Notification.prototype[zoneSymbol('addEventListener')]).toBeTruthy(); + }); + })); diff --git a/packages/zone.js/test/browser/WebSocket.spec.ts b/packages/zone.js/test/browser/WebSocket.spec.ts new file mode 100644 index 0000000000..77b2308abe --- /dev/null +++ b/packages/zone.js/test/browser/WebSocket.spec.ts @@ -0,0 +1,138 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from '../test-util'; +declare const window: any; + +const TIMEOUT = 5000; + +if (!window['saucelabs']) { + // sauceLabs does not support WebSockets; skip these tests + + xdescribe('WebSocket', ifEnvSupports('WebSocket', function() { + let socket: WebSocket; + const TEST_SERVER_URL = 'ws://localhost:8001'; + const testZone = Zone.current.fork({name: 'test'}); + + + beforeEach(function(done) { + socket = new WebSocket(TEST_SERVER_URL); + socket.addEventListener('open', function() { done(); }); + socket.addEventListener('error', function() { + fail( + 'Can\'t establish socket to ' + TEST_SERVER_URL + + '! do you have test/ws-server.js running?'); + done(); + }); + }, TIMEOUT); + + afterEach(function(done) { + socket.addEventListener('close', function() { done(); }); + socket.close(); + }, TIMEOUT); + + xit('should be patched in a Web Worker', done => { + const worker = new Worker('/base/test/ws-webworker-context.js'); + worker.onmessage = (e: MessageEvent) => { + if (e.data !== 'pass' && e.data !== 'fail') { + fail(`web worker ${e.data}`); + return; + } + expect(e.data).toBe('pass'); + done(); + }; + }, 10000); + + it('should work with addEventListener', function(done) { + testZone.run(function() { + socket.addEventListener('message', function(event) { + expect(Zone.current).toBe(testZone); + expect(event['data']).toBe('hi'); + done(); + }); + }); + socket.send('hi'); + }, TIMEOUT); + + + it('should respect removeEventListener', function(done) { + let log = ''; + + function logOnMessage() { + log += 'a'; + + expect(log).toEqual('a'); + + socket.removeEventListener('message', logOnMessage); + socket.send('hi'); + + setTimeout(function() { + expect(log).toEqual('a'); + done(); + }, 10); + } + + socket.addEventListener('message', logOnMessage); + socket.send('hi'); + }, TIMEOUT); + + + it('should work with onmessage', function(done) { + testZone.run(function() { + socket.onmessage = function(contents) { + expect(Zone.current).toBe(testZone); + expect(contents.data).toBe('hi'); + done(); + }; + }); + socket.send('hi'); + }, TIMEOUT); + + + it('should only allow one onmessage handler', function(done) { + let log = ''; + + socket.onmessage = function() { + log += 'a'; + expect(log).toEqual('b'); + done(); + }; + + socket.onmessage = function() { + log += 'b'; + expect(log).toEqual('b'); + done(); + }; + + socket.send('hi'); + }, TIMEOUT); + + + it('should handler removing onmessage', function(done) { + let log = ''; + + socket.onmessage = function() { log += 'a'; }; + + socket.onmessage = null as any; + + socket.send('hi'); + + setTimeout(function() { + expect(log).toEqual(''); + done(); + }, 100); + }, TIMEOUT); + + it('should have constants', function() { + expect(Object.keys(WebSocket)).toContain('CONNECTING'); + expect(Object.keys(WebSocket)).toContain('OPEN'); + expect(Object.keys(WebSocket)).toContain('CLOSING'); + expect(Object.keys(WebSocket)).toContain('CLOSED'); + }); + })); +} diff --git a/packages/zone.js/test/browser/Worker.spec.ts b/packages/zone.js/test/browser/Worker.spec.ts new file mode 100644 index 0000000000..27681ac8e0 --- /dev/null +++ b/packages/zone.js/test/browser/Worker.spec.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {zoneSymbol} from '../../lib/common/utils'; +import {asyncTest, ifEnvSupports} from '../test-util'; + +function workerSupport() { + const Worker = (window as any)['Worker']; + if (!Worker) { + return false; + } + const desc = Object.getOwnPropertyDescriptor(Worker.prototype, 'onmessage'); + if (!desc || !desc.configurable) { + return false; + } + return true; +} + +(workerSupport as any).message = 'Worker Support'; + +xdescribe('Worker API', ifEnvSupports(workerSupport, function() { + it('Worker API should be patched by Zone', asyncTest((done: Function) => { + const zone: Zone = Zone.current.fork({name: 'worker'}); + zone.run(() => { + const worker = + new Worker('/base/angular/packages/zone.js/test/assets/worker.js'); + worker.onmessage = function(evt: MessageEvent) { + expect(evt.data).toEqual('worker'); + expect(Zone.current.name).toEqual('worker'); + done(); + }; + }); + }, Zone.root)); + })); diff --git a/packages/zone.js/test/browser/XMLHttpRequest.spec.ts b/packages/zone.js/test/browser/XMLHttpRequest.spec.ts new file mode 100644 index 0000000000..d70f90462f --- /dev/null +++ b/packages/zone.js/test/browser/XMLHttpRequest.spec.ts @@ -0,0 +1,381 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports, ifEnvSupportsWithDone, supportPatchXHROnProperty, zoneSymbol} from '../test-util'; +declare const global: any; +const wtfMock = global.wtfMock; + +describe('XMLHttpRequest', function() { + let testZone: Zone; + + beforeEach(() => { testZone = Zone.current.fork({name: 'test'}); }); + + it('should intercept XHRs and treat them as MacroTasks', function(done) { + let req: XMLHttpRequest; + let onStable: any; + const testZoneWithWtf = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({ + name: 'TestZone', + onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => { + if (!hasTask.macroTask) { + onStable && onStable(); + } + } + }); + + testZoneWithWtf.run(() => { + req = new XMLHttpRequest(); + const logs: string[] = []; + req.onload = () => { logs.push('onload'); }; + onStable = function() { + expect(wtfMock.log[wtfMock.log.length - 2]) + .toEqual('> Zone:invokeTask:XMLHttpRequest.send("::ProxyZone::WTF::TestZone")'); + expect(wtfMock.log[wtfMock.log.length - 1]) + .toEqual('< Zone:invokeTask:XMLHttpRequest.send'); + if (supportPatchXHROnProperty()) { + expect(wtfMock.log[wtfMock.log.length - 3]) + .toMatch(/\< Zone\:invokeTask.*addEventListener\:load/); + expect(wtfMock.log[wtfMock.log.length - 4]) + .toMatch(/\> Zone\:invokeTask.*addEventListener\:load/); + } + // if browser can patch onload + if ((req as any)[zoneSymbol('loadfalse')]) { + expect(logs).toEqual(['onload']); + } + done(); + }; + + req.open('get', '/', true); + req.send(); + const lastScheduled = wtfMock.log[wtfMock.log.length - 1]; + expect(lastScheduled).toMatch('# Zone:schedule:macroTask:XMLHttpRequest.send'); + }, null, undefined, 'unit-test'); + }); + + it('should not trigger Zone callback of internal onreadystatechange', function(done) { + const scheduleSpy = jasmine.createSpy('schedule'); + const xhrZone = Zone.current.fork({ + name: 'xhr', + onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone, task: Task) => { + if (task.type === 'eventTask') { + scheduleSpy(task.source); + } + return delegate.scheduleTask(targetZone, task); + } + }); + + xhrZone.run(() => { + const req = new XMLHttpRequest(); + req.onload = function() { + expect(Zone.current.name).toEqual('xhr'); + if (supportPatchXHROnProperty()) { + expect(scheduleSpy).toHaveBeenCalled(); + } + done(); + }; + req.open('get', '/', true); + req.send(); + }); + }); + + it('should work with onreadystatechange', function(done) { + let req: XMLHttpRequest; + + testZone.run(function() { + req = new XMLHttpRequest(); + req.onreadystatechange = function() { + // Make sure that the wrapCallback will only be called once + req.onreadystatechange = null as any; + expect(Zone.current).toBe(testZone); + done(); + }; + req.open('get', '/', true); + }); + + req !.send(); + }); + + it('should return null when access ontimeout first time without error', function() { + let req: XMLHttpRequest = new XMLHttpRequest(); + expect(req.ontimeout).toBe(null); + }); + + const supportsOnProgress = function() { return 'onprogress' in (new XMLHttpRequest()); }; + + (supportsOnProgress).message = 'XMLHttpRequest.onprogress'; + + describe('onprogress', ifEnvSupports(supportsOnProgress, function() { + it('should work with onprogress', function(done) { + let req: XMLHttpRequest; + testZone.run(function() { + req = new XMLHttpRequest(); + req.onprogress = function() { + // Make sure that the wrapCallback will only be called once + req.onprogress = null as any; + expect(Zone.current).toBe(testZone); + done(); + }; + req.open('get', '/', true); + }); + + req !.send(); + }); + + it('should allow canceling of an XMLHttpRequest', function(done) { + const spy = jasmine.createSpy('spy'); + let req: XMLHttpRequest; + let pending = false; + + const trackingTestZone = Zone.current.fork({ + name: 'tracking test zone', + onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone, + hasTaskState: HasTaskState) => { + if (hasTaskState.change == 'macroTask') { + pending = hasTaskState.macroTask; + } + delegate.hasTask(target, hasTaskState); + } + }); + + trackingTestZone.run(function() { + req = new XMLHttpRequest(); + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status !== 0) { + spy(); + } + } + }; + req.open('get', '/', true); + + req.send(); + req.abort(); + }); + + setTimeout(function() { + expect(spy).not.toHaveBeenCalled(); + expect(pending).toEqual(false); + done(); + }, 0); + }); + + it('should allow aborting an XMLHttpRequest after its completed', function(done) { + let req: XMLHttpRequest; + + testZone.run(function() { + req = new XMLHttpRequest(); + req.onreadystatechange = function() { + if (req.readyState === XMLHttpRequest.DONE) { + if (req.status !== 0) { + setTimeout(function() { + req.abort(); + done(); + }, 0); + } + } + }; + req.open('get', '/', true); + + req.send(); + }); + }); + })); + + it('should preserve other setters', function() { + const req = new XMLHttpRequest(); + req.open('get', '/', true); + req.send(); + try { + req.responseType = 'document'; + expect(req.responseType).toBe('document'); + } catch (e) { + // Android browser: using this setter throws, this should be preserved + expect(e.message).toBe('INVALID_STATE_ERR: DOM Exception 11'); + } + }); + + it('should work with synchronous XMLHttpRequest', function() { + const log: HasTaskState[] = []; + Zone.current + .fork({ + name: 'sync-xhr-test', + onHasTask: function( + delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) { + log.push(hasTaskState); + delegate.hasTask(target, hasTaskState); + } + }) + .run(() => { + const req = new XMLHttpRequest(); + req.open('get', '/', false); + req.send(); + }); + expect(log).toEqual([]); + }); + + it('should preserve static constants', function() { + expect(XMLHttpRequest.UNSENT).toEqual(0); + expect(XMLHttpRequest.OPENED).toEqual(1); + expect(XMLHttpRequest.HEADERS_RECEIVED).toEqual(2); + expect(XMLHttpRequest.LOADING).toEqual(3); + expect(XMLHttpRequest.DONE).toEqual(4); + }); + + it('should work properly when send request multiple times on single xmlRequest instance', + function(done) { + testZone.run(function() { + const req = new XMLHttpRequest(); + req.open('get', '/', true); + req.send(); + req.onload = function() { + req.onload = null as any; + req.open('get', '/', true); + req.onload = function() { done(); }; + expect(() => { req.send(); }).not.toThrow(); + }; + }); + }); + + it('should keep taskcount correctly when abort was called multiple times before request is done', + function(done) { + testZone.run(function() { + const req = new XMLHttpRequest(); + req.open('get', '/', true); + req.send(); + req.addEventListener('readystatechange', function(ev) { + if (req.readyState >= 2) { + expect(() => { req.abort(); }).not.toThrow(); + done(); + } + }); + }); + }); + + it('should trigger readystatechange if xhr request trigger cors error', (done) => { + const req = new XMLHttpRequest(); + let err: any = null; + try { + req.open('get', 'file:///test', true); + } catch (err) { + // in IE, open will throw Access is denied error + done(); + return; + } + req.addEventListener('readystatechange', function(ev) { + if (req.readyState === 4) { + const xhrScheduled = (req as any)[zoneSymbol('xhrScheduled')]; + const task = (req as any)[zoneSymbol('xhrTask')]; + if (xhrScheduled === false) { + expect(task.state).toEqual('scheduling'); + setTimeout(() => { + if (err) { + expect(task.state).toEqual('unknown'); + } else { + expect(task.state).toEqual('notScheduled'); + } + done(); + }); + } else { + expect(task.state).toEqual('scheduled'); + done(); + } + } + }); + try { + req.send(); + } catch (error) { + err = error; + } + }); + + it('should invoke task if xhr request trigger cors error', (done) => { + const logs: string[] = []; + const zone = Zone.current.fork({ + name: 'xhr', + onHasTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, hasTask: HasTaskState) => { + logs.push(JSON.stringify(hasTask)); + } + }); + const req = new XMLHttpRequest(); + try { + req.open('get', 'file:///test', true); + } catch (err) { + // in IE, open will throw Access is denied error + done(); + return; + } + zone.run(() => { + let isError = false; + let timerId = null; + try { + timerId = (window as any)[zoneSymbol('setTimeout')](() => { + expect(logs).toEqual([ + `{"microTask":false,"macroTask":true,"eventTask":false,"change":"macroTask"}`, + `{"microTask":false,"macroTask":false,"eventTask":false,"change":"macroTask"}` + ]); + done(); + }, 500); + req.send(); + } catch (error) { + isError = true; + (window as any)[zoneSymbol('clearTimeout')](timerId); + done(); + } + }); + }); + + it('should not throw error when get XMLHttpRequest.prototype.onreadystatechange the first time', + function() { + const func = function() { + testZone.run(function() { + const req = new XMLHttpRequest(); + req.onreadystatechange; + }); + }; + expect(func).not.toThrow(); + }); + + it('should be in the zone when use XMLHttpRequest.addEventListener', function(done) { + testZone.run(function() { + // sometimes this case will cause timeout + // so we set it longer + const interval = (jasmine).DEFAULT_TIMEOUT_INTERVAL; + (jasmine).DEFAULT_TIMEOUT_INTERVAL = 5000; + const req = new XMLHttpRequest(); + req.open('get', '/', true); + req.addEventListener('readystatechange', function() { + if (req.readyState === 4) { + // expect(Zone.current.name).toEqual('test'); + (jasmine).DEFAULT_TIMEOUT_INTERVAL = interval; + done(); + } + }); + req.send(); + }); + }); + + it('should return origin listener when call xhr.onreadystatechange', + ifEnvSupportsWithDone(supportPatchXHROnProperty, function(done: Function) { + testZone.run(function() { + // sometimes this case will cause timeout + // so we set it longer + const req = new XMLHttpRequest(); + req.open('get', '/', true); + const interval = (jasmine).DEFAULT_TIMEOUT_INTERVAL; + (jasmine).DEFAULT_TIMEOUT_INTERVAL = 5000; + const listener = req.onreadystatechange = function() { + if (req.readyState === 4) { + (jasmine).DEFAULT_TIMEOUT_INTERVAL = interval; + done(); + } + }; + expect(req.onreadystatechange).toBe(listener); + req.onreadystatechange = function() { return listener.call(this); }; + req.send(); + }); + })); +}); diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts new file mode 100644 index 0000000000..bc41ea7810 --- /dev/null +++ b/packages/zone.js/test/browser/browser.spec.ts @@ -0,0 +1,2363 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {patchFilteredProperties} from '../../lib/browser/property-descriptor'; +import {patchEventTarget} from '../../lib/common/events'; +import {isIEOrEdge, zoneSymbol} from '../../lib/common/utils'; +import {getEdgeVersion, getIEVersion, ifEnvSupports, ifEnvSupportsWithDone, isEdge} from '../test-util'; + +import Spy = jasmine.Spy; +declare const global: any; + +const noop = function() {}; + +function windowPrototype() { + return !!(global['Window'] && global['Window'].prototype); +} + +function promiseUnhandleRejectionSupport() { + return !!global['PromiseRejectionEvent']; +} + +function canPatchOnProperty(obj: any, prop: string) { + const func = function() { + if (!obj) { + return false; + } + const desc = Object.getOwnPropertyDescriptor(obj, prop); + if (!desc || !desc.configurable) { + return false; + } + return true; + }; + + (func as any).message = 'patchOnProperties'; + return func; +} + +let supportsPassive = false; +try { + const opts = Object.defineProperty({}, 'passive', {get: function() { supportsPassive = true; }}); + window.addEventListener('test', opts as any, opts); + window.removeEventListener('test', opts as any, opts); +} catch (e) { +} + +function supportEventListenerOptions() { + return supportsPassive; +} + +(supportEventListenerOptions as any).message = 'supportsEventListenerOptions'; + +function supportCanvasTest() { + const HTMLCanvasElement = (window as any)['HTMLCanvasElement']; + const supportCanvas = typeof HTMLCanvasElement !== 'undefined' && HTMLCanvasElement.prototype && + HTMLCanvasElement.prototype.toBlob; + const FileReader = (window as any)['FileReader']; + const supportFileReader = typeof FileReader !== 'undefined'; + return supportCanvas && supportFileReader; +} + +(supportCanvasTest as any).message = 'supportCanvasTest'; + +function ieOrEdge() { + return isIEOrEdge(); +} + +(ieOrEdge as any).message = 'IE/Edge Test'; + +class TestEventListener { + logs: any[] = []; + addEventListener(eventName: string, listener: any, options: any) { this.logs.push(options); } + removeEventListener(eventName: string, listener: any, options: any) {} +} + +describe('Zone', function() { + const rootZone = Zone.current; + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + + describe('hooks', function() { + it('should allow you to override alert/prompt/confirm', function() { + const alertSpy = jasmine.createSpy('alert'); + const promptSpy = jasmine.createSpy('prompt'); + const confirmSpy = jasmine.createSpy('confirm'); + const spies: {[k: string]: + Function} = {'alert': alertSpy, 'prompt': promptSpy, 'confirm': confirmSpy}; + const myZone = Zone.current.fork({ + name: 'spy', + onInvoke: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + callback: Function, applyThis?: any, applyArgs?: any[], + source?: string): any => { + if (source) { + spies[source].apply(null, applyArgs); + } else { + return parentZoneDelegate.invoke(targetZone, callback, applyThis, applyArgs, source); + } + } + }); + + myZone.run(function() { + alert('alertMsg'); + prompt('promptMsg', 'default'); + confirm('confirmMsg'); + }); + + expect(alertSpy).toHaveBeenCalledWith('alertMsg'); + expect(promptSpy).toHaveBeenCalledWith('promptMsg', 'default'); + expect(confirmSpy).toHaveBeenCalledWith('confirmMsg'); + }); + + describe( + 'DOM onProperty hooks', + ifEnvSupports(canPatchOnProperty(HTMLElement.prototype, 'onclick'), function() { + let mouseEvent = document.createEvent('Event'); + let hookSpy: Spy, eventListenerSpy: Spy; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + beforeEach(function() { + mouseEvent.initEvent('mousedown', true, true); + hookSpy = jasmine.createSpy('hook'); + eventListenerSpy = jasmine.createSpy('eventListener'); + }); + + function checkIsOnPropertiesPatched(target: any, ignoredProperties?: string[]) { + for (let prop in target) { + if (ignoredProperties && + ignoredProperties.filter(ignoreProp => ignoreProp === prop).length > 0) { + continue; + } + if (prop.substr(0, 2) === 'on' && prop.length > 2) { + target[prop] = noop; + if (!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]) { + console.log('onProp is null:', prop); + } else { + target[prop] = null; + expect(!target[Zone.__symbol__('ON_PROPERTY' + prop.substr(2))]).toBeTruthy(); + } + } + } + } + + it('should patch all possbile on properties on element', function() { + const htmlElementTagNames: string[] = [ + 'a', 'area', 'audio', 'base', 'basefont', 'blockquote', 'br', + 'button', 'canvas', 'caption', 'col', 'colgroup', 'data', 'datalist', + 'del', 'dir', 'div', 'dl', 'embed', 'fieldset', 'font', + 'form', 'frame', 'frameset', 'h1', 'h2', 'h3', 'h4', + 'h5', 'h6', 'head', 'hr', 'html', 'iframe', 'img', + 'input', 'ins', 'isindex', 'label', 'legend', 'li', 'link', + 'listing', 'map', 'marquee', 'menu', 'meta', 'meter', 'nextid', + 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', + 'pre', 'progress', 'q', 'script', 'select', 'source', 'span', + 'style', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', + 'th', 'thead', 'time', 'title', 'tr', 'track', 'ul', + 'video' + ]; + htmlElementTagNames.forEach(tagName => { + checkIsOnPropertiesPatched(document.createElement(tagName), ['onorientationchange']); + }); + }); + + it('should patch all possbile on properties on body', + function() { checkIsOnPropertiesPatched(document.body, ['onorientationchange']); }); + + it('should patch all possbile on properties on Document', + function() { checkIsOnPropertiesPatched(document, ['onorientationchange']); }); + + it('should patch all possbile on properties on Window', function() { + checkIsOnPropertiesPatched(window, [ + 'onvrdisplayactivate', 'onvrdisplayblur', 'onvrdisplayconnect', + 'onvrdisplaydeactivate', 'onvrdisplaydisconnect', 'onvrdisplayfocus', + 'onvrdisplaypointerrestricted', 'onvrdisplaypointerunrestricted', + 'onorientationchange', 'onerror' + ]); + }); + + it('should patch all possbile on properties on xhr', + function() { checkIsOnPropertiesPatched(new XMLHttpRequest()); }); + + it('should not patch ignored on properties', function() { + const TestTarget: any = (window as any)['TestTarget']; + patchFilteredProperties( + TestTarget.prototype, ['prop1', 'prop2'], global['__Zone_ignore_on_properties']); + const testTarget = new TestTarget(); + Zone.current.fork({name: 'test'}).run(() => { + testTarget.onprop1 = function() { + // onprop1 should not be patched + expect(Zone.current.name).toEqual('test1'); + }; + testTarget.onprop2 = function() { + // onprop2 should be patched + expect(Zone.current.name).toEqual('test'); + }; + }); + + Zone.current.fork({name: 'test1'}).run(() => { + testTarget.dispatchEvent('prop1'); + testTarget.dispatchEvent('prop2'); + }); + }); + + it('should not patch ignored eventListener', function() { + let scrollEvent = document.createEvent('Event'); + scrollEvent.initEvent('scroll', true, true); + + const zone = Zone.current.fork({name: 'run'}); + + Zone.current.fork({name: 'scroll'}).run(() => { + document.addEventListener( + 'scroll', () => { expect(Zone.current.name).toEqual(zone.name); }); + }); + + zone.run(() => { document.dispatchEvent(scrollEvent); }); + }); + + it('should be able to clear on handler added before load zone.js', function() { + const TestTarget: any = (window as any)['TestTarget']; + patchFilteredProperties( + TestTarget.prototype, ['prop3'], global['__Zone_ignore_on_properties']); + const testTarget = new TestTarget(); + Zone.current.fork({name: 'test'}).run(() => { + expect(testTarget.onprop3).toBeTruthy(); + const newProp3Handler = function() {}; + testTarget.onprop3 = newProp3Handler; + expect(testTarget.onprop3).toBe(newProp3Handler); + testTarget.onprop3 = null; + expect(!testTarget.onprop3).toBeTruthy(); + testTarget.onprop3 = function() { + // onprop1 should not be patched + expect(Zone.current.name).toEqual('test'); + }; + }); + + Zone.current.fork({name: 'test1'}).run(() => { testTarget.dispatchEvent('prop3'); }); + }); + + it('window onclick should be in zone', + ifEnvSupports(canPatchOnProperty(window, 'onmousedown'), function() { + zone.run(function() { window.onmousedown = eventListenerSpy; }); + + window.dispatchEvent(mouseEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(eventListenerSpy).toHaveBeenCalled(); + window.removeEventListener('mousedown', eventListenerSpy); + })); + + it('window onresize should be patched', + ifEnvSupports(canPatchOnProperty(window, 'onmousedown'), function() { + window.onresize = eventListenerSpy; + const innerResizeProp: any = (window as any)[zoneSymbol('ON_PROPERTYresize')]; + expect(innerResizeProp).toBeTruthy(); + innerResizeProp(); + expect(eventListenerSpy).toHaveBeenCalled(); + window.removeEventListener('resize', eventListenerSpy); + })); + + it('document onclick should be in zone', + ifEnvSupports(canPatchOnProperty(Document.prototype, 'onmousedown'), function() { + zone.run(function() { document.onmousedown = eventListenerSpy; }); + + document.dispatchEvent(mouseEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(eventListenerSpy).toHaveBeenCalled(); + document.removeEventListener('mousedown', eventListenerSpy); + })); + + // TODO: JiaLiPassion, need to find out why the test bundle is not `use strict`. + xit('event handler with null context should use event.target', + ifEnvSupports(canPatchOnProperty(Document.prototype, 'onmousedown'), function() { + const ieVer = getIEVersion(); + if (ieVer && ieVer === 9) { + // in ie9, this is window object even we call func.apply(undefined) + return; + } + const logs: string[] = []; + const EventTarget = (window as any)['EventTarget']; + let oriAddEventListener = EventTarget && EventTarget.prototype ? + (EventTarget.prototype as any)[zoneSymbol('addEventListener')] : + (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')]; + + if (!oriAddEventListener) { + // no patched addEventListener found + return; + } + let handler1: Function; + let handler2: Function; + + const listener = function() { logs.push('listener1'); }; + + const listener1 = function() { logs.push('listener2'); }; + + HTMLSpanElement.prototype.addEventListener = function( + eventName: string, callback: any) { + if (eventName === 'click') { + handler1 = callback; + } else if (eventName === 'mousedown') { + handler2 = callback; + } + return oriAddEventListener.apply(this, arguments); + }; + + (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')] = null; + + patchEventTarget(window, [HTMLSpanElement.prototype]); + + const span = document.createElement('span'); + document.body.appendChild(span); + + zone.run(function() { + span.addEventListener('click', listener); + span.onmousedown = listener1; + }); + + expect(handler1 !).toBe(handler2 !); + + handler1 !.apply(null, [{type: 'click', target: span}]); + + handler2 !.apply(null, [{type: 'mousedown', target: span}]); + + expect(hookSpy).toHaveBeenCalled(); + expect(logs).toEqual(['listener1', 'listener2']); + document.body.removeChild(span); + if (EventTarget) { + (EventTarget.prototype as any)[zoneSymbol('addEventListener')] = + oriAddEventListener; + } else { + (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')] = + oriAddEventListener; + } + })); + + it('SVGElement onclick should be in zone', + ifEnvSupports( + canPatchOnProperty(SVGElement && SVGElement.prototype, 'onmousedown'), function() { + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + document.body.appendChild(svg); + zone.run(function() { svg.onmousedown = eventListenerSpy; }); + + svg.dispatchEvent(mouseEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(eventListenerSpy).toHaveBeenCalled(); + svg.removeEventListener('mouse', eventListenerSpy); + document.body.removeChild(svg); + })); + + it('get window onerror should not throw error', + ifEnvSupports(canPatchOnProperty(window, 'onerror'), function() { + const testFn = function() { + let onerror = window.onerror; + window.onerror = function() {}; + onerror = window.onerror; + }; + expect(testFn).not.toThrow(); + })); + + it('window.onerror callback signiture should be (message, source, lineno, colno, error)', + ifEnvSupportsWithDone(canPatchOnProperty(window, 'onerror'), function(done: DoneFn) { + let testError = new Error('testError'); + window.onerror = function( + message: any, source?: string, lineno?: number, colno?: number, error?: any) { + expect(message).toContain('testError'); + if (getEdgeVersion() !== 14) { + // Edge 14, error will be undefined. + expect(error).toBe(testError); + } + (window as any).onerror = null; + setTimeout(done); + return true; + }; + setTimeout(() => { throw testError; }, 100); + })); + })); + + describe('eventListener hooks', function() { + let button: HTMLButtonElement; + let clickEvent: Event; + + beforeEach(function() { + button = document.createElement('button'); + clickEvent = document.createEvent('Event'); + clickEvent.initEvent('click', true, true); + document.body.appendChild(button); + }); + + afterEach(function() { document.body.removeChild(button); }); + + it('should support addEventListener', function() { + const hookSpy = jasmine.createSpy('hook'); + const eventListenerSpy = jasmine.createSpy('eventListener'); + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(function() { button.addEventListener('click', eventListenerSpy); }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(eventListenerSpy).toHaveBeenCalled(); + }); + + it('should be able to access addEventListener information in onScheduleTask', function() { + const hookSpy = jasmine.createSpy('hook'); + const eventListenerSpy = jasmine.createSpy('eventListener'); + let scheduleButton; + let scheduleEventName: string|undefined; + let scheduleCapture: boolean|undefined; + let scheduleTask; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + scheduleButton = (task.data as any).taskData.target; + scheduleEventName = (task.data as any).taskData.eventName; + scheduleCapture = (task.data as any).taskData.capture; + scheduleTask = task; + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(function() { button.addEventListener('click', eventListenerSpy); }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(eventListenerSpy).toHaveBeenCalled(); + expect(scheduleButton).toBe(button as any); + expect(scheduleEventName).toBe('click'); + expect(scheduleCapture).toBe(false); + expect(scheduleTask && (scheduleTask as any).data.taskData).toBe(null as any); + }); + + it('should support addEventListener on window', ifEnvSupports(windowPrototype, function() { + const hookSpy = jasmine.createSpy('hook'); + const eventListenerSpy = jasmine.createSpy('eventListener'); + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(function() { window.addEventListener('click', eventListenerSpy); }); + + window.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(eventListenerSpy).toHaveBeenCalled(); + })); + + it('should support removeEventListener', function() { + const hookSpy = jasmine.createSpy('hook'); + const eventListenerSpy = jasmine.createSpy('eventListener'); + const zone = rootZone.fork({ + name: 'spy', + onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.cancelTask(targetZone, task); + } + }); + + zone.run(function() { + button.addEventListener('click', eventListenerSpy); + button.removeEventListener('click', eventListenerSpy); + }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(eventListenerSpy).not.toHaveBeenCalled(); + }); + + describe( + 'should support addEventListener/removeEventListener with AddEventListenerOptions with capture setting', + ifEnvSupports(supportEventListenerOptions, function() { + let hookSpy: Spy; + let cancelSpy: Spy; + let logs: string[]; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, + targetZone: Zone, task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + }, + onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + cancelSpy(); + return parentZoneDelegate.cancelTask(targetZone, task); + } + }); + + const docListener = () => { logs.push('document'); }; + const btnListener = () => { logs.push('button'); }; + + beforeEach(() => { + logs = []; + hookSpy = jasmine.createSpy('hook'); + cancelSpy = jasmine.createSpy('cancel'); + }); + + it('should handle child event when addEventListener with capture true', () => { + // test capture true + zone.run(function() { + (document as any).addEventListener('click', docListener, {capture: true}); + button.addEventListener('click', btnListener); + }); + + button.dispatchEvent(clickEvent); + expect(hookSpy).toHaveBeenCalled(); + + expect(logs).toEqual(['document', 'button']); + logs = []; + + (document as any).removeEventListener('click', docListener, {capture: true}); + button.removeEventListener('click', btnListener); + expect(cancelSpy).toHaveBeenCalled(); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual([]); + }); + + it('should handle child event when addEventListener with capture true', () => { + // test capture false + zone.run(function() { + (document as any).addEventListener('click', docListener, {capture: false}); + button.addEventListener('click', btnListener); + }); + + button.dispatchEvent(clickEvent); + expect(hookSpy).toHaveBeenCalled(); + expect(logs).toEqual(['button', 'document']); + logs = []; + + (document as any).removeEventListener('click', docListener, {capture: false}); + button.removeEventListener('click', btnListener); + expect(cancelSpy).toHaveBeenCalled(); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual([]); + }); + })); + + describe( + 'should ignore duplicate event handler', + ifEnvSupports(supportEventListenerOptions, function() { + let hookSpy: Spy; + let cancelSpy: Spy; + let logs: string[]; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, + targetZone: Zone, task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + }, + onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + cancelSpy(); + return parentZoneDelegate.cancelTask(targetZone, task); + } + }); + + const docListener = () => { logs.push('document options'); }; + + beforeEach(() => { + logs = []; + hookSpy = jasmine.createSpy('hook'); + cancelSpy = jasmine.createSpy('cancel'); + }); + + const testDuplicate = function(args1?: any, args2?: any) { + zone.run(function() { + if (args1) { + (document as any).addEventListener('click', docListener, args1); + } else { + (document as any).addEventListener('click', docListener); + } + if (args2) { + (document as any).addEventListener('click', docListener, args2); + } else { + (document as any).addEventListener('click', docListener); + } + }); + + button.dispatchEvent(clickEvent); + expect(hookSpy).toHaveBeenCalled(); + expect(logs).toEqual(['document options']); + logs = []; + + (document as any).removeEventListener('click', docListener, args1); + expect(cancelSpy).toHaveBeenCalled(); + button.dispatchEvent(clickEvent); + expect(logs).toEqual([]); + }; + + it('should ignore duplicate handler', () => { + let captureFalse = [ + undefined, false, {capture: false}, {capture: false, passive: false}, + {passive: false}, {} + ]; + let captureTrue = [true, {capture: true}, {capture: true, passive: false}]; + for (let i = 0; i < captureFalse.length; i++) { + for (let j = 0; j < captureFalse.length; j++) { + testDuplicate(captureFalse[i], captureFalse[j]); + } + } + for (let i = 0; i < captureTrue.length; i++) { + for (let j = 0; j < captureTrue.length; j++) { + testDuplicate(captureTrue[i], captureTrue[j]); + } + } + }); + })); + + describe( + 'should support mix useCapture with AddEventListenerOptions capture', + ifEnvSupports(supportEventListenerOptions, function() { + let hookSpy: Spy; + let cancelSpy: Spy; + let logs: string[]; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, + targetZone: Zone, task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + }, + onCancelTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + cancelSpy(); + return parentZoneDelegate.cancelTask(targetZone, task); + } + }); + + const docListener = () => { logs.push('document options'); }; + const docListener1 = () => { logs.push('document useCapture'); }; + const btnListener = () => { logs.push('button'); }; + + beforeEach(() => { + logs = []; + hookSpy = jasmine.createSpy('hook'); + cancelSpy = jasmine.createSpy('cancel'); + }); + + const testAddRemove = function(args1?: any, args2?: any) { + zone.run(function() { + if (args1) { + (document as any).addEventListener('click', docListener, args1); + } else { + (document as any).addEventListener('click', docListener); + } + if (args2) { + (document as any).removeEventListener('click', docListener, args2); + } else { + (document as any).removeEventListener('click', docListener); + } + }); + + button.dispatchEvent(clickEvent); + expect(cancelSpy).toHaveBeenCalled(); + expect(logs).toEqual([]); + }; + + it('should be able to add/remove same handler with mix options and capture', + function() { + let captureFalse = [ + undefined, false, {capture: false}, {capture: false, passive: false}, + {passive: false}, {} + ]; + let captureTrue = [true, {capture: true}, {capture: true, passive: false}]; + for (let i = 0; i < captureFalse.length; i++) { + for (let j = 0; j < captureFalse.length; j++) { + testAddRemove(captureFalse[i], captureFalse[j]); + } + } + for (let i = 0; i < captureTrue.length; i++) { + for (let j = 0; j < captureTrue.length; j++) { + testAddRemove(captureTrue[i], captureTrue[j]); + } + } + }); + + const testDifferent = function(args1?: any, args2?: any) { + zone.run(function() { + if (args1) { + (document as any).addEventListener('click', docListener, args1); + } else { + (document as any).addEventListener('click', docListener); + } + if (args2) { + (document as any).addEventListener('click', docListener1, args2); + } else { + (document as any).addEventListener('click', docListener1); + } + }); + + button.dispatchEvent(clickEvent); + expect(hookSpy).toHaveBeenCalled(); + expect(logs.sort()).toEqual(['document options', 'document useCapture']); + logs = []; + + if (args1) { + (document as any).removeEventListener('click', docListener, args1); + } else { + (document as any).removeEventListener('click', docListener); + } + + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['document useCapture']); + logs = []; + + if (args2) { + (document as any).removeEventListener('click', docListener1, args2); + } else { + (document as any).removeEventListener('click', docListener1); + } + + button.dispatchEvent(clickEvent); + expect(logs).toEqual([]); + }; + + it('should be able to add different handlers for same event', function() { + let captureFalse = [ + undefined, false, {capture: false}, {capture: false, passive: false}, + {passive: false}, {} + ]; + let captureTrue = [true, {capture: true}, {capture: true, passive: false}]; + for (let i = 0; i < captureFalse.length; i++) { + for (let j = 0; j < captureTrue.length; j++) { + testDifferent(captureFalse[i], captureTrue[j]); + } + } + for (let i = 0; i < captureTrue.length; i++) { + for (let j = 0; j < captureFalse.length; j++) { + testDifferent(captureTrue[i], captureFalse[j]); + } + } + }); + + it('should handle options.capture true with capture true correctly', function() { + zone.run(function() { + (document as any).addEventListener('click', docListener, {capture: true}); + document.addEventListener('click', docListener1, true); + button.addEventListener('click', btnListener); + }); + + button.dispatchEvent(clickEvent); + expect(hookSpy).toHaveBeenCalled(); + expect(logs).toEqual(['document options', 'document useCapture', 'button']); + logs = []; + + (document as any).removeEventListener('click', docListener, {capture: true}); + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['document useCapture', 'button']); + logs = []; + + document.removeEventListener('click', docListener1, true); + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['button']); + logs = []; + + button.removeEventListener('click', btnListener); + expect(cancelSpy).toHaveBeenCalled(); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual([]); + }); + })); + + it('should support addEventListener with AddEventListenerOptions once setting', + ifEnvSupports(supportEventListenerOptions, function() { + let hookSpy = jasmine.createSpy('hook'); + let logs: string[] = []; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(function() { + (button as any).addEventListener('click', function() { + logs.push('click'); + }, {once: true}); + }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(logs.length).toBe(1); + expect(logs).toEqual(['click']); + logs = []; + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + })); + + it('should support addEventListener with AddEventListenerOptions once setting and capture', + ifEnvSupports(supportEventListenerOptions, function() { + let hookSpy = jasmine.createSpy('hook'); + let logs: string[] = []; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(function() { + (button as any).addEventListener('click', function() { + logs.push('click'); + }, {once: true, capture: true}); + }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(logs.length).toBe(1); + expect(logs).toEqual(['click']); + logs = []; + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + })); + + + it('should support add multipe listeners with AddEventListenerOptions once setting and same capture after normal listener', + ifEnvSupports(supportEventListenerOptions, function() { + let logs: string[] = []; + + button.addEventListener('click', function() { logs.push('click'); }, true); + (button as any).addEventListener('click', function() { + logs.push('once click'); + }, {once: true, capture: true}); + + button.dispatchEvent(clickEvent); + + expect(logs.length).toBe(2); + expect(logs).toEqual(['click', 'once click']); + logs = []; + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['click']); + })); + + it('should support add multipe listeners with AddEventListenerOptions once setting and mixed capture after normal listener', + ifEnvSupports(supportEventListenerOptions, function() { + let logs: string[] = []; + + button.addEventListener('click', function() { logs.push('click'); }); + (button as any).addEventListener('click', function() { + logs.push('once click'); + }, {once: true, capture: true}); + + button.dispatchEvent(clickEvent); + + expect(logs.length).toBe(2); + expect(logs).toEqual(['click', 'once click']); + logs = []; + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['click']); + })); + + it('should support add multipe listeners with AddEventListenerOptions once setting before normal listener', + ifEnvSupports(supportEventListenerOptions, function() { + let logs: string[] = []; + + (button as any).addEventListener('click', function() { + logs.push('once click'); + }, {once: true}); + + button.addEventListener('click', function() { logs.push('click'); }); + + button.dispatchEvent(clickEvent); + + expect(logs.length).toBe(2); + expect(logs).toEqual(['once click', 'click']); + logs = []; + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['click']); + })); + + it('should support add multipe listeners with AddEventListenerOptions once setting with same capture before normal listener', + ifEnvSupports(supportEventListenerOptions, function() { + let logs: string[] = []; + + (button as any).addEventListener('click', function() { + logs.push('once click'); + }, {once: true, capture: true}); + + button.addEventListener('click', function() { logs.push('click'); }, true); + + button.dispatchEvent(clickEvent); + + expect(logs.length).toBe(2); + expect(logs).toEqual(['once click', 'click']); + logs = []; + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['click']); + })); + + it('should support add multipe listeners with AddEventListenerOptions once setting with mixed capture before normal listener', + ifEnvSupports(supportEventListenerOptions, function() { + let logs: string[] = []; + + (button as any).addEventListener('click', function() { + logs.push('once click'); + }, {once: true, capture: true}); + + button.addEventListener('click', function() { logs.push('click'); }); + + button.dispatchEvent(clickEvent); + + expect(logs.length).toBe(2); + expect(logs).toEqual(['once click', 'click']); + logs = []; + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['click']); + })); + + it('should change options to boolean if not support passive', () => { + patchEventTarget(window, [TestEventListener.prototype]); + const testEventListener = new TestEventListener(); + + const listener = function() {}; + testEventListener.addEventListener('test', listener, {passive: true}); + testEventListener.addEventListener('test1', listener, {once: true}); + testEventListener.addEventListener('test2', listener, {capture: true}); + testEventListener.addEventListener('test3', listener, {passive: false}); + testEventListener.addEventListener('test4', listener, {once: false}); + testEventListener.addEventListener('test5', listener, {capture: false}); + if (!supportsPassive) { + expect(testEventListener.logs).toEqual([false, false, true, false, false, false]); + } else { + expect(testEventListener.logs).toEqual([ + {passive: true}, {once: true}, {capture: true}, {passive: false}, {once: false}, + {capture: false} + ]); + } + }); + + it('should change options to boolean if not support passive on HTMLElement', () => { + const logs: string[] = []; + const listener = (e: Event) => { logs.push('clicked'); }; + + (button as any).addEventListener('click', listener, {once: true}); + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['clicked']); + button.dispatchEvent(clickEvent); + if (supportsPassive) { + expect(logs).toEqual(['clicked']); + } else { + expect(logs).toEqual(['clicked', 'clicked']); + } + + button.removeEventListener('click', listener); + }); + + it('should support addEventListener with AddEventListenerOptions passive setting', + ifEnvSupports(supportEventListenerOptions, function() { + const hookSpy = jasmine.createSpy('hook'); + const logs: string[] = []; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + const listener = (e: Event) => { + logs.push(e.defaultPrevented.toString()); + e.preventDefault(); + logs.push(e.defaultPrevented.toString()); + }; + + zone.run(function() { + (button as any).addEventListener('click', listener, {passive: true}); + }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(logs).toEqual(['false', 'false']); + + button.removeEventListener('click', listener); + })); + + it('should support Event.stopImmediatePropagation', + ifEnvSupports(supportEventListenerOptions, function() { + const hookSpy = jasmine.createSpy('hook'); + const logs: any[] = []; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + const listener1 = (e: Event) => { + logs.push('listener1'); + e.stopImmediatePropagation(); + }; + + const listener2 = (e: Event) => { logs.push('listener2'); }; + + zone.run(function() { + (button as any).addEventListener('click', listener1); + (button as any).addEventListener('click', listener2); + }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).toHaveBeenCalled(); + expect(logs).toEqual(['listener1']); + + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener2); + })); + + it('should support remove event listener by call zone.cancelTask directly', function() { + let logs: string[] = []; + let eventTask: Task; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + eventTask = task; + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(() => { button.addEventListener('click', function() { logs.push('click'); }); }); + let listeners = (button as any).eventListeners('click'); + expect(listeners.length).toBe(1); + eventTask !.zone.cancelTask(eventTask !); + + listeners = (button as any).eventListeners('click'); + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + expect(listeners.length).toBe(0); + }); + + it('should support remove event listener by call zone.cancelTask directly with capture=true', + function() { + let logs: string[] = []; + let eventTask: Task; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + eventTask = task; + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(() => { + button.addEventListener('click', function() { logs.push('click'); }, true); + }); + let listeners = (button as any).eventListeners('click'); + expect(listeners.length).toBe(1); + eventTask !.zone.cancelTask(eventTask !); + + listeners = (button as any).eventListeners('click'); + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + expect(listeners.length).toBe(0); + }); + + it('should support remove event listeners by call zone.cancelTask directly with multiple listeners', + function() { + let logs: string[] = []; + let eventTask: Task; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + eventTask = task; + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run( + () => { button.addEventListener('click', function() { logs.push('click1'); }); }); + button.addEventListener('click', function() { logs.push('click2'); }); + let listeners = (button as any).eventListeners('click'); + expect(listeners.length).toBe(2); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['click1', 'click2']); + eventTask !.zone.cancelTask(eventTask !); + logs = []; + + listeners = (button as any).eventListeners('click'); + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(listeners.length).toBe(1); + expect(logs).toEqual(['click2']); + }); + + it('should support remove event listeners by call zone.cancelTask directly with multiple listeners with same capture=true', + function() { + let logs: string[] = []; + let eventTask: Task; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + eventTask = task; + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(() => { + button.addEventListener('click', function() { logs.push('click1'); }, true); + }); + button.addEventListener('click', function() { logs.push('click2'); }, true); + let listeners = (button as any).eventListeners('click'); + expect(listeners.length).toBe(2); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['click1', 'click2']); + eventTask !.zone.cancelTask(eventTask !); + logs = []; + + listeners = (button as any).eventListeners('click'); + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(listeners.length).toBe(1); + expect(logs).toEqual(['click2']); + }); + + it('should support remove event listeners by call zone.cancelTask directly with multiple listeners with mixed capture', + function() { + let logs: string[] = []; + let eventTask: Task; + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + eventTask = task; + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(() => { + button.addEventListener('click', function() { logs.push('click1'); }, true); + }); + button.addEventListener('click', function() { logs.push('click2'); }); + let listeners = (button as any).eventListeners('click'); + expect(listeners.length).toBe(2); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['click1', 'click2']); + eventTask !.zone.cancelTask(eventTask !); + logs = []; + + listeners = (button as any).eventListeners('click'); + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(listeners.length).toBe(1); + expect(logs).toEqual(['click2']); + }); + + it('should support reschedule eventTask', + ifEnvSupports(supportEventListenerOptions, function() { + let hookSpy1 = jasmine.createSpy('spy1'); + let hookSpy2 = jasmine.createSpy('spy2'); + let hookSpy3 = jasmine.createSpy('spy3'); + let logs: string[] = []; + const isBlacklistedEvent = function(source: string) { + return source.lastIndexOf('click') !== -1; + }; + const zone1 = Zone.current.fork({ + name: 'zone1', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + if ((task.type === 'eventTask' || task.type === 'macroTask') && + isBlacklistedEvent(task.source)) { + task.cancelScheduleRequest(); + + return zone2.scheduleTask(task); + } else { + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }, + onInvokeTask( + parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task, + applyThis: any, applyArgs: any) { + hookSpy1(); + return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs); + } + }); + const zone2 = Zone.current.fork({ + name: 'zone2', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy2(); + return parentZoneDelegate.scheduleTask(targetZone, task); + }, + onInvokeTask( + parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task, + applyThis: any, applyArgs: any) { + hookSpy3(); + return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs); + } + }); + + const listener = function() { logs.push(Zone.current.name); }; + zone1.run(() => { + button.addEventListener('click', listener); + button.addEventListener('mouseover', listener); + }); + + const clickEvent = document.createEvent('Event'); + clickEvent.initEvent('click', true, true); + const mouseEvent = document.createEvent('Event'); + mouseEvent.initEvent('mouseover', true, true); + + button.dispatchEvent(clickEvent); + button.removeEventListener('click', listener); + + expect(logs).toEqual(['zone2']); + expect(hookSpy1).not.toHaveBeenCalled(); + expect(hookSpy2).toHaveBeenCalled(); + expect(hookSpy3).toHaveBeenCalled(); + logs = []; + hookSpy2 = jasmine.createSpy('hookSpy2'); + hookSpy3 = jasmine.createSpy('hookSpy3'); + + button.dispatchEvent(mouseEvent); + button.removeEventListener('mouseover', listener); + expect(logs).toEqual(['zone1']); + expect(hookSpy1).toHaveBeenCalled(); + expect(hookSpy2).not.toHaveBeenCalled(); + expect(hookSpy3).not.toHaveBeenCalled(); + })); + + it('should support inline event handler attributes', function() { + const hookSpy = jasmine.createSpy('hook'); + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + zone.run(function() { + button.setAttribute('onclick', 'return'); + expect(button.onclick).not.toBe(null); + }); + }); + + describe('should be able to remove eventListener during eventListener callback', function() { + it('should be able to remove eventListener during eventListener callback', function() { + let logs: string[] = []; + const listener1 = function() { + button.removeEventListener('click', listener1); + logs.push('listener1'); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener2', 'listener3']); + + button.removeEventListener('click', listener2); + button.removeEventListener('click', listener3); + }); + + it('should be able to remove eventListener during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { + button.removeEventListener('click', listener1, true); + logs.push('listener1'); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener2', 'listener3']); + + button.removeEventListener('click', listener2, true); + button.removeEventListener('click', listener3, true); + }); + + it('should be able to remove handleEvent eventListener during eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener3); + } + }; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener2']); + + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener2); + }); + + it('should be able to remove handleEvent eventListener during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener3, true); + } + }; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener2']); + + button.removeEventListener('click', listener1, true); + button.removeEventListener('click', listener2, true); + }); + + it('should be able to remove multiple eventListeners during eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { + logs.push('listener1'); + button.removeEventListener('click', listener2); + button.removeEventListener('click', listener3); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener1']); + + button.removeEventListener('click', listener1); + }); + + it('should be able to remove multiple eventListeners during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { + logs.push('listener1'); + button.removeEventListener('click', listener2, true); + button.removeEventListener('click', listener3, true); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener1']); + + button.removeEventListener('click', listener1, true); + }); + + it('should be able to remove part of other eventListener during eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { + logs.push('listener1'); + button.removeEventListener('click', listener2); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener3']); + + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener3); + }); + + it('should be able to remove part of other eventListener during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { + logs.push('listener1'); + button.removeEventListener('click', listener2, true); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener3']); + + button.removeEventListener('click', listener1, true); + button.removeEventListener('click', listener3, true); + }); + + it('should be able to remove all beforeward and afterward eventListener during eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { + logs.push('listener2'); + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener3); + }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener2']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener2']); + + button.removeEventListener('click', listener2); + }); + + it('should be able to remove all beforeward and afterward eventListener during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { + logs.push('listener2'); + button.removeEventListener('click', listener1, true); + button.removeEventListener('click', listener3, true); + }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener2']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener2']); + + button.removeEventListener('click', listener2, true); + }); + + it('should be able to remove part of beforeward and afterward eventListener during eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener2); + button.removeEventListener('click', listener4); + } + }; + const listener4 = function() { logs.push('listener4'); }; + const listener5 = function() { logs.push('listener5'); }; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + button.addEventListener('click', listener4); + button.addEventListener('click', listener5); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(4); + expect(logs).toEqual(['listener1', 'listener2', 'listener3', 'listener5']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener3', 'listener5']); + + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener3); + button.removeEventListener('click', listener5); + }); + + it('should be able to remove part of beforeward and afterward eventListener during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener2, true); + button.removeEventListener('click', listener4, true); + } + }; + const listener4 = function() { logs.push('listener4'); }; + const listener5 = function() { logs.push('listener5'); }; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + button.addEventListener('click', listener4, true); + button.addEventListener('click', listener5, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(4); + expect(logs).toEqual(['listener1', 'listener2', 'listener3', 'listener5']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener3', 'listener5']); + + button.removeEventListener('click', listener1, true); + button.removeEventListener('click', listener3, true); + button.removeEventListener('click', listener5, true); + }); + + it('should be able to remove all beforeward eventListener during eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener2); + } + }; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener3']); + + button.removeEventListener('click', listener3); + }); + + it('should be able to remove all beforeward eventListener during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener1, true); + button.removeEventListener('click', listener2, true); + } + }; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener3']); + + button.removeEventListener('click', listener3, true); + }); + + it('should be able to remove part of beforeward eventListener during eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener1); + } + }; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener2', 'listener3']); + + button.removeEventListener('click', listener2); + button.removeEventListener('click', listener3); + }); + + it('should be able to remove part of beforeward eventListener during eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + button.removeEventListener('click', listener1, true); + } + }; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener2', 'listener3']); + + button.removeEventListener('click', listener2, true); + button.removeEventListener('click', listener3, true); + }); + + it('should be able to remove all eventListeners during first eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { + (button as any).removeAllListeners('click'); + logs.push('listener1'); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener1']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + }); + + it('should be able to remove all eventListeners during first eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { + (button as any).removeAllListeners('click'); + logs.push('listener1'); + }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(1); + expect(logs).toEqual(['listener1']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + }); + + it('should be able to remove all eventListeners during middle eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { + (button as any).removeAllListeners('click'); + logs.push('listener2'); + }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener2']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + }); + + it('should be able to remove all eventListeners during middle eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { + (button as any).removeAllListeners('click'); + logs.push('listener2'); + }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(2); + expect(logs).toEqual(['listener1', 'listener2']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + }); + + it('should be able to remove all eventListeners during last eventListener callback', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + (button as any).removeAllListeners('click'); + } + }; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + }); + + it('should be able to remove all eventListeners during last eventListener callback with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = { + handleEvent: function(event: Event) { + logs.push('listener3'); + (button as any).removeAllListeners('click'); + } + }; + + button.addEventListener('click', listener1, true); + button.addEventListener('click', listener2, true); + button.addEventListener('click', listener3, true); + + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(3); + expect(logs).toEqual(['listener1', 'listener2', 'listener3']); + + logs = []; + button.dispatchEvent(clickEvent); + expect(logs.length).toBe(0); + }); + }); + + it('should be able to get eventListeners of specified event form EventTarget', function() { + const listener1 = function() {}; + const listener2 = function() {}; + const listener3 = {handleEvent: function(event: Event) {}}; + const listener4 = function() {}; + + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + button.addEventListener('click', listener3); + button.addEventListener('mouseover', listener4); + + const listeners = (button as any).eventListeners('click'); + expect(listeners.length).toBe(3); + expect(listeners).toEqual([listener1, listener2, listener3]); + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener2); + button.removeEventListener('click', listener3); + }); + + it('should be able to get all eventListeners form EventTarget without eventName', function() { + const listener1 = function() {}; + const listener2 = function() {}; + const listener3 = {handleEvent: function(event: Event) {}}; + + button.addEventListener('click', listener1); + button.addEventListener('mouseover', listener2); + button.addEventListener('mousehover', listener3); + + const listeners = (button as any).eventListeners(); + expect(listeners.length).toBe(3); + expect(listeners).toEqual([listener1, listener2, listener3]); + button.removeEventListener('click', listener1); + button.removeEventListener('mouseover', listener2); + button.removeEventListener('mousehover', listener3); + }); + + it('should be able to remove all listeners of specified event form EventTarget', function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + const listener4 = function() { logs.push('listener4'); }; + + button.addEventListener('mouseover', listener1); + button.addEventListener('mouseover', listener2); + button.addEventListener('mouseover', listener3); + button.addEventListener('click', listener4); + + (button as any).removeAllListeners('mouseover'); + const listeners = (button as any).eventListeners('mouseove'); + expect(listeners.length).toBe(0); + + const mouseEvent = document.createEvent('Event'); + mouseEvent.initEvent('mouseover', true, true); + + button.dispatchEvent(mouseEvent); + expect(logs).toEqual([]); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['listener4']); + + button.removeEventListener('click', listener4); + }); + + it('should be able to remove all listeners of specified event form EventTarget with capture=true', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + const listener4 = function() { logs.push('listener4'); }; + + button.addEventListener('mouseover', listener1, true); + button.addEventListener('mouseover', listener2, true); + button.addEventListener('mouseover', listener3, true); + button.addEventListener('click', listener4, true); + + (button as any).removeAllListeners('mouseover'); + const listeners = (button as any).eventListeners('mouseove'); + expect(listeners.length).toBe(0); + + const mouseEvent = document.createEvent('Event'); + mouseEvent.initEvent('mouseover', true, true); + + button.dispatchEvent(mouseEvent); + expect(logs).toEqual([]); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['listener4']); + + button.removeEventListener('click', listener4); + }); + + it('should be able to remove all listeners of specified event form EventTarget with mixed capture', + function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + const listener4 = function() { logs.push('listener4'); }; + + button.addEventListener('mouseover', listener1, true); + button.addEventListener('mouseover', listener2, false); + button.addEventListener('mouseover', listener3, true); + button.addEventListener('click', listener4, true); + + (button as any).removeAllListeners('mouseover'); + const listeners = (button as any).eventListeners('mouseove'); + expect(listeners.length).toBe(0); + + const mouseEvent = document.createEvent('Event'); + mouseEvent.initEvent('mouseover', true, true); + + button.dispatchEvent(mouseEvent); + expect(logs).toEqual([]); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['listener4']); + + button.removeEventListener('click', listener4); + }); + + it('should be able to remove all listeners of all events form EventTarget', function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + const listener4 = function() { logs.push('listener4'); }; + + button.addEventListener('mouseover', listener1); + button.addEventListener('mouseover', listener2); + button.addEventListener('mouseover', listener3); + button.addEventListener('click', listener4); + + (button as any).removeAllListeners(); + const listeners = (button as any).eventListeners('mouseover'); + expect(listeners.length).toBe(0); + + const mouseEvent = document.createEvent('Event'); + mouseEvent.initEvent('mouseover', true, true); + + button.dispatchEvent(mouseEvent); + expect(logs).toEqual([]); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual([]); + }); + + it('should be able to remove listener which was added outside of zone ', function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + const listener4 = function() { logs.push('listener4'); }; + + button.addEventListener('mouseover', listener1); + (button as any)[Zone.__symbol__('addEventListener')]('mouseover', listener2); + button.addEventListener('click', listener3); + (button as any)[Zone.__symbol__('addEventListener')]('click', listener4); + + button.removeEventListener('mouseover', listener1); + button.removeEventListener('mouseover', listener2); + button.removeEventListener('click', listener3); + button.removeEventListener('click', listener4); + const listeners = (button as any).eventListeners('mouseover'); + expect(listeners.length).toBe(0); + + const mouseEvent = document.createEvent('Event'); + mouseEvent.initEvent('mouseover', true, true); + + button.dispatchEvent(mouseEvent); + expect(logs).toEqual([]); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual([]); + }); + + it('should be able to remove all listeners which were added inside of zone ', function() { + let logs: string[] = []; + const listener1 = function() { logs.push('listener1'); }; + const listener2 = function() { logs.push('listener2'); }; + const listener3 = {handleEvent: function(event: Event) { logs.push('listener3'); }}; + const listener4 = function() { logs.push('listener4'); }; + + button.addEventListener('mouseover', listener1); + (button as any)[Zone.__symbol__('addEventListener')]('mouseover', listener2); + button.addEventListener('click', listener3); + (button as any)[Zone.__symbol__('addEventListener')]('click', listener4); + + (button as any).removeAllListeners(); + const listeners = (button as any).eventListeners('mouseover'); + expect(listeners.length).toBe(0); + + const mouseEvent = document.createEvent('Event'); + mouseEvent.initEvent('mouseover', true, true); + + button.dispatchEvent(mouseEvent); + expect(logs).toEqual(['listener2']); + + button.dispatchEvent(clickEvent); + expect(logs).toEqual(['listener2', 'listener4']); + }); + + it('should bypass addEventListener of FunctionWrapper and __BROWSERTOOLS_CONSOLE_SAFEFUNC of IE/Edge', + ifEnvSupports(ieOrEdge, function() { + const hookSpy = jasmine.createSpy('hook'); + const zone = rootZone.fork({ + name: 'spy', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + hookSpy(); + return parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + let logs: string[] = []; + + const listener1 = function() { logs.push(Zone.current.name); }; + + (listener1 as any).toString = function() { return '[object FunctionWrapper]'; }; + + const listener2 = function() { logs.push(Zone.current.name); }; + + (listener2 as any).toString = function() { + return 'function __BROWSERTOOLS_CONSOLE_SAFEFUNC() { [native code] }'; + }; + + zone.run(() => { + button.addEventListener('click', listener1); + button.addEventListener('click', listener2); + }); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).not.toHaveBeenCalled(); + expect(logs).toEqual(['ProxyZone', 'ProxyZone']); + logs = []; + + button.removeEventListener('click', listener1); + button.removeEventListener('click', listener2); + + button.dispatchEvent(clickEvent); + + expect(hookSpy).not.toHaveBeenCalled(); + expect(logs).toEqual([]); + })); + }); + + describe('unhandle promise rejection', () => { + const AsyncTestZoneSpec = (Zone as any)['AsyncTestZoneSpec']; + const asyncTest = function(testFn: Function) { + return (done: Function) => { + let asyncTestZone: Zone = Zone.current.fork( + new AsyncTestZoneSpec(done, (error: Error) => { fail(error); }, 'asyncTest')); + asyncTestZone.run(testFn); + }; + }; + + it('should support window.addEventListener(unhandledrejection)', asyncTest(() => { + if (!promiseUnhandleRejectionSupport()) { + return; + } + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.root.fork({name: 'promise'}).run(function() { + const listener = (evt: any) => { + window.removeEventListener('unhandledrejection', listener); + expect(evt.type).toEqual('unhandledrejection'); + expect(evt.promise.constructor.name).toEqual('Promise'); + expect(evt.reason.message).toBe('promise error'); + }; + window.addEventListener('unhandledrejection', listener); + new Promise((resolve, reject) => { throw new Error('promise error'); }); + }); + })); + + it('should support window.addEventListener(rejectionhandled)', asyncTest(() => { + if (!promiseUnhandleRejectionSupport()) { + return; + } + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.root.fork({name: 'promise'}).run(function() { + const listener = (evt: any) => { + window.removeEventListener('unhandledrejection', listener); + p.catch(reason => {}); + }; + window.addEventListener('unhandledrejection', listener); + + const handledListener = (evt: any) => { + window.removeEventListener('rejectionhandled', handledListener); + expect(evt.type).toEqual('rejectionhandled'); + expect(evt.promise.constructor.name).toEqual('Promise'); + expect(evt.reason.message).toBe('promise error'); + }; + + window.addEventListener('rejectionhandled', handledListener); + const p = new Promise((resolve, reject) => { throw new Error('promise error'); }); + }); + })); + + it('should support multiple window.addEventListener(unhandledrejection)', asyncTest(() => { + if (!promiseUnhandleRejectionSupport()) { + return; + } + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.root.fork({name: 'promise'}).run(function() { + const listener1 = (evt: any) => { + window.removeEventListener('unhandledrejection', listener1); + expect(evt.type).toEqual('unhandledrejection'); + expect(evt.promise.constructor.name).toEqual('Promise'); + expect(evt.reason.message).toBe('promise error'); + }; + const listener2 = (evt: any) => { + window.removeEventListener('unhandledrejection', listener2); + expect(evt.type).toEqual('unhandledrejection'); + expect(evt.promise.constructor.name).toEqual('Promise'); + expect(evt.reason.message).toBe('promise error'); + }; + window.addEventListener('unhandledrejection', listener1); + window.addEventListener('unhandledrejection', listener2); + new Promise((resolve, reject) => { throw new Error('promise error'); }); + }); + })); + }); + + // @JiaLiPassion, Edge 15, the behavior is not the same with Chrome + // wait for fix. + xit('IntersectionObserver should run callback in zone', + ifEnvSupportsWithDone('IntersectionObserver', (done: Function) => { + const div = document.createElement('div'); + document.body.appendChild(div); + const options: any = {threshold: 0.5}; + + const zone = Zone.current.fork({name: 'intersectionObserverZone'}); + + zone.run(() => { + const observer = new IntersectionObserver(() => { + expect(Zone.current.name).toEqual(zone.name); + observer.unobserve(div); + done(); + }, options); + observer.observe(div); + }); + div.style.display = 'none'; + div.style.visibility = 'block'; + })); + + it('HTMLCanvasElement.toBlob should be a ZoneAware MacroTask', + ifEnvSupportsWithDone(supportCanvasTest, (done: Function) => { + const canvas = document.createElement('canvas'); + const d = canvas.width; + const ctx = canvas.getContext('2d') !; + ctx.beginPath(); + ctx.moveTo(d / 2, 0); + ctx.lineTo(d, d); + ctx.lineTo(0, d); + ctx.closePath(); + ctx.fillStyle = 'yellow'; + ctx.fill(); + + const scheduleSpy = jasmine.createSpy('scheduleSpy'); + const zone: Zone = Zone.current.fork({ + name: 'canvas', + onScheduleTask: + (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) => { + scheduleSpy(); + return delegate.scheduleTask(targetZone, task); + } + }); + + zone.run(() => { + const canvasData = canvas.toDataURL(); + canvas.toBlob(function(blob) { + expect(Zone.current.name).toEqual('canvas'); + expect(scheduleSpy).toHaveBeenCalled(); + + const reader = new FileReader(); + reader.readAsDataURL(blob !); + reader.onloadend = function() { + const base64data = reader.result; + expect(base64data).toEqual(canvasData); + done(); + }; + }); + }); + })); + + describe( + 'ResizeObserver', ifEnvSupports('ResizeObserver', () => { + it('ResizeObserver callback should be in zone', (done) => { + const ResizeObserver = (window as any)['ResizeObserver']; + const div = document.createElement('div'); + const zone = Zone.current.fork({name: 'observer'}); + const observer = new ResizeObserver((entries: any, ob: any) => { + expect(Zone.current.name).toEqual(zone.name); + + expect(entries.length).toBe(1); + expect(entries[0].target).toBe(div); + done(); + }); + + zone.run(() => { observer.observe(div); }); + + document.body.appendChild(div); + }); + + it('ResizeObserver callback should be able to in different zones which when they were observed', + (done) => { + const ResizeObserver = (window as any)['ResizeObserver']; + const div1 = document.createElement('div'); + const div2 = document.createElement('div'); + const zone = Zone.current.fork({name: 'observer'}); + let count = 0; + const observer = new ResizeObserver((entries: any, ob: any) => { + entries.forEach((entry: any) => { + if (entry.target === div1) { + expect(Zone.current.name).toEqual(zone.name); + } else { + expect(Zone.current.name).toEqual(''); + } + }); + count++; + if (count === 2) { + done(); + } + }); + + zone.run(() => { observer.observe(div1); }); + Zone.root.run(() => { observer.observe(div2); }); + + document.body.appendChild(div1); + document.body.appendChild(div2); + }); + })); + + xdescribe('getUserMedia', () => { + it('navigator.mediaDevices.getUserMedia should in zone', + ifEnvSupportsWithDone( + () => { + return !isEdge() && navigator && navigator.mediaDevices && + typeof navigator.mediaDevices.getUserMedia === 'function'; + }, + (done: Function) => { + const zone = Zone.current.fork({name: 'media'}); + zone.run(() => { + const constraints = {audio: true, video: {width: 1280, height: 720}}; + + navigator.mediaDevices.getUserMedia(constraints) + .then(function(mediaStream) { + expect(Zone.current.name).toEqual(zone.name); + done(); + }) + .catch(function(err) { + console.log(err.name + ': ' + err.message); + expect(Zone.current.name).toEqual(zone.name); + done(); + }); + }); + })); + + it('navigator.getUserMedia should in zone', + ifEnvSupportsWithDone( + () => { + return !isEdge() && navigator && typeof navigator.getUserMedia === 'function'; + }, + (done: Function) => { + const zone = Zone.current.fork({name: 'media'}); + zone.run(() => { + const constraints = {audio: true, video: {width: 1280, height: 720}}; + navigator.getUserMedia( + constraints, + () => { + expect(Zone.current.name).toEqual(zone.name); + done(); + }, + () => { + expect(Zone.current.name).toEqual(zone.name); + done(); + }); + }); + })); + }); + }); +}); diff --git a/packages/zone.js/test/browser/custom-element.spec.js b/packages/zone.js/test/browser/custom-element.spec.js new file mode 100644 index 0000000000..a5456c48eb --- /dev/null +++ b/packages/zone.js/test/browser/custom-element.spec.js @@ -0,0 +1,96 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/* + * check that document.registerElement(name, { prototype: proto }); + * is properly patched + */ + +function customElementsSupport() { + return 'registerElement' in document; +} +customElementsSupport.message = 'window.customElements'; + +describe('customElements', function() { + const testZone = Zone.current.fork({name: 'test'}); + const bridge = { + connectedCallback: () => {}, + disconnectedCallback: () => {}, + adoptedCallback: () => {}, + attributeChangedCallback: () => {} + }; + + class TestCustomElement extends HTMLElement { + constructor() { super(); } + + static get observedAttributes() { return ['attr1', 'attr2']; } + + connectedCallback() { return bridge.connectedCallback(); } + + disconnectedCallback() { return bridge.disconnectedCallback(); } + + attributeChangedCallback(attrName, oldVal, newVal) { + return bridge.attributeChangedCallback(attrName, oldVal, newVal); + } + + adoptedCallback() { return bridge.adoptedCallback(); } + } + + testZone.run(() => { customElements.define('x-test', TestCustomElement); }); + + let elt; + + beforeEach(() => { + bridge.connectedCallback = () => {}; + bridge.disconnectedCallback = () => {}; + bridge.attributeChangedCallback = () => {}; + bridge.adoptedCallback = () => {}; + }); + + afterEach(() => { + if (elt) { + document.body.removeChild(elt); + elt = null; + } + }); + + it('should work with connectedCallback', function(done) { + bridge.connectedCallback = function() { + expect(Zone.current.name).toBe(testZone.name); + done(); + }; + + elt = document.createElement('x-test'); + document.body.appendChild(elt); + }); + + it('should work with disconnectedCallback', function(done) { + bridge.disconnectedCallback = function() { + expect(Zone.current.name).toBe(testZone.name); + done(); + }; + + elt = document.createElement('x-test'); + document.body.appendChild(elt); + document.body.removeChild(elt); + elt = null; + }); + + it('should work with attributeChanged', function(done) { + bridge.attributeChangedCallback = function(attrName, oldVal, newVal) { + expect(Zone.current.name).toBe(testZone.name); + expect(attrName).toEqual('attr1'); + expect(newVal).toEqual('value1'); + done(); + }; + + elt = document.createElement('x-test'); + document.body.appendChild(elt); + elt.setAttribute('attr1', 'value1'); + }); +}); diff --git a/packages/zone.js/test/browser/define-property.spec.ts b/packages/zone.js/test/browser/define-property.spec.ts new file mode 100644 index 0000000000..f9032fa108 --- /dev/null +++ b/packages/zone.js/test/browser/define-property.spec.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +describe('defineProperty', function() { + it('should not throw when defining length on an array', function() { + const someArray: any[] = []; + expect(() => Object.defineProperty(someArray, 'length', {value: 2, writable: false})) + .not.toThrow(); + }); + + it('should not throw error when try to defineProperty with a frozen desc', function() { + const obj = {}; + const desc = Object.freeze({value: null, writable: true}); + Object.defineProperty(obj, 'prop', desc); + }); + + it('should not throw error when try to defineProperty with a frozen obj', function() { + const obj = {}; + Object.freeze(obj); + Object.defineProperty(obj, 'prop', {configurable: true, writable: true, value: 'value'}); + }); +}); \ No newline at end of file diff --git a/packages/zone.js/test/browser/element.spec.ts b/packages/zone.js/test/browser/element.spec.ts new file mode 100644 index 0000000000..540c0c275d --- /dev/null +++ b/packages/zone.js/test/browser/element.spec.ts @@ -0,0 +1,312 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from '../test-util'; + +describe('element', function() { + let button: HTMLButtonElement; + + beforeEach(function() { + button = document.createElement('button'); + document.body.appendChild(button); + }); + + afterEach(function() { document.body.removeChild(button); }); + + // https://github.com/angular/zone.js/issues/190 + it('should work when addEventListener / removeEventListener are called in the global context', + function() { + const clickEvent = document.createEvent('Event'); + let callCount = 0; + + clickEvent.initEvent('click', true, true); + + const listener = function(event: Event) { + callCount++; + expect(event).toBe(clickEvent); + }; + + // `this` would be null inside the method when `addEventListener` is called from strict mode + // it would be `window`: + // - when called from non strict-mode, + // - when `window.addEventListener` is called explicitly. + addEventListener('click', listener); + + button.dispatchEvent(clickEvent); + expect(callCount).toEqual(1); + + removeEventListener('click', listener); + button.dispatchEvent(clickEvent); + expect(callCount).toEqual(1); + }); + + it('should work with addEventListener when called with a function listener', function() { + const clickEvent = document.createEvent('Event'); + clickEvent.initEvent('click', true, true); + + button.addEventListener('click', function(event) { expect(event).toBe(clickEvent as any); }); + + button.dispatchEvent(clickEvent); + }); + + it('should not call microtasks early when an event is invoked', function(done) { + let log = ''; + button.addEventListener('click', () => { + Zone.current.scheduleMicroTask('test', () => log += 'microtask;'); + log += 'click;'; + }); + button.click(); + + expect(log).toEqual('click;'); + done(); + }); + + it('should call microtasks early when an event is invoked', function(done) { + /* + * In this test we escape the Zone using unpatched setTimeout. + * This way the eventTask invoked from click will think it is the top most + * task and eagerly drain the microtask queue. + * + * THIS IS THE WRONG BEHAVIOR! + * + * But there is no easy way for the task to know if it is the top most task. + * + * Given that this can only arise when someone is emulating clicks on DOM in a synchronous + * fashion we have few choices: + * 1. Ignore as this is unlikely to be a problem outside of tests. + * 2. Monkey patch the event methods to increment the _numberOfNestedTaskFrames and prevent + * eager drainage. + * 3. Pay the cost of throwing an exception in event tasks and verifying that we are the + * top most frame. + * + * For now we are choosing to ignore it and assume that this arises in tests only. + * As an added measure we make sure that all jasmine tests always run in a task. See: jasmine.ts + */ + (window as any)[(Zone as any).__symbol__('setTimeout')](() => { + let log = ''; + button.addEventListener('click', () => { + Zone.current.scheduleMicroTask('test', () => log += 'microtask;'); + log += 'click;'; + }); + button.click(); + + expect(log).toEqual('click;microtask;'); + done(); + }); + }); + + it('should work with addEventListener when called with an EventListener-implementing listener', + function() { + const eventListener = { + x: 5, + handleEvent: function(event: Event) { + // Test that context is preserved + expect(this.x).toBe(5); + + expect(event).toBe(clickEvent); + } + }; + + const clickEvent = document.createEvent('Event'); + clickEvent.initEvent('click', true, true); + + button.addEventListener('click', eventListener); + + button.dispatchEvent(clickEvent); + }); + + it('should respect removeEventListener when called with a function listener', function() { + let log = ''; + const logFunction = function logFunction() { log += 'a'; }; + + button.addEventListener('click', logFunction); + button.addEventListener('focus', logFunction); + button.click(); + expect(log).toEqual('a'); + const focusEvent = document.createEvent('Event'); + focusEvent.initEvent('focus', true, true); + button.dispatchEvent(focusEvent); + expect(log).toEqual('aa'); + + button.removeEventListener('click', logFunction); + button.click(); + expect(log).toEqual('aa'); + }); + + it('should respect removeEventListener with an EventListener-implementing listener', function() { + const eventListener = {x: 5, handleEvent: jasmine.createSpy('handleEvent')}; + + button.addEventListener('click', eventListener); + button.removeEventListener('click', eventListener); + + button.click(); + + expect(eventListener.handleEvent).not.toHaveBeenCalled(); + }); + + it('should have no effect while calling addEventListener without listener', function() { + const onAddEventListenerSpy = jasmine.createSpy('addEventListener'); + const eventListenerZone = + Zone.current.fork({name: 'eventListenerZone', onScheduleTask: onAddEventListenerSpy}); + expect(function() { + eventListenerZone.run(function() { + button.addEventListener('click', null as any); + button.addEventListener('click', undefined as any); + }); + }).not.toThrowError(); + expect(onAddEventListenerSpy).not.toHaveBeenCalledWith(); + }); + + it('should have no effect while calling removeEventListener without listener', function() { + const onAddEventListenerSpy = jasmine.createSpy('removeEventListener'); + const eventListenerZone = + Zone.current.fork({name: 'eventListenerZone', onScheduleTask: onAddEventListenerSpy}); + expect(function() { + eventListenerZone.run(function() { + button.removeEventListener('click', null as any); + button.removeEventListener('click', undefined as any); + }); + }).not.toThrowError(); + expect(onAddEventListenerSpy).not.toHaveBeenCalledWith(); + }); + + + it('should only add a listener once for a given set of arguments', function() { + const log: string[] = []; + const clickEvent = document.createEvent('Event'); + + function listener() { log.push('listener'); } + + clickEvent.initEvent('click', true, true); + + button.addEventListener('click', listener); + button.addEventListener('click', listener); + button.addEventListener('click', listener); + + button.dispatchEvent(clickEvent); + expect(log).toEqual(['listener']); + + button.removeEventListener('click', listener); + + button.dispatchEvent(clickEvent); + expect(log).toEqual(['listener']); + }); + + it('should correctly handler capturing versus nonCapturing eventListeners', function() { + const log: string[] = []; + const clickEvent = document.createEvent('Event'); + + function capturingListener() { log.push('capturingListener'); } + + function bubblingListener() { log.push('bubblingListener'); } + + clickEvent.initEvent('click', true, true); + + document.body.addEventListener('click', capturingListener, true); + document.body.addEventListener('click', bubblingListener); + + button.dispatchEvent(clickEvent); + + expect(log).toEqual(['capturingListener', 'bubblingListener']); + }); + + it('should correctly handler a listener that is both capturing and nonCapturing', function() { + const log: string[] = []; + const clickEvent = document.createEvent('Event'); + + function listener() { log.push('listener'); } + + clickEvent.initEvent('click', true, true); + + document.body.addEventListener('click', listener, true); + document.body.addEventListener('click', listener); + + button.dispatchEvent(clickEvent); + + document.body.removeEventListener('click', listener, true); + document.body.removeEventListener('click', listener); + + button.dispatchEvent(clickEvent); + + expect(log).toEqual(['listener', 'listener']); + }); + + describe('onclick', function() { + function supportsOnClick() { + const div = document.createElement('div'); + const clickPropDesc = Object.getOwnPropertyDescriptor(div, 'onclick'); + return !( + EventTarget && div instanceof EventTarget && clickPropDesc && + clickPropDesc.value === null); + } + (supportsOnClick).message = 'Supports Element#onclick patching'; + + + ifEnvSupports(supportsOnClick, function() { + it('should spawn new child zones', function() { + let run = false; + button.onclick = function() { run = true; }; + + button.click(); + expect(run).toBeTruthy(); + }); + }); + + + it('should only allow one onclick handler', function() { + let log = ''; + button.onclick = function() { log += 'a'; }; + button.onclick = function() { log += 'b'; }; + + button.click(); + expect(log).toEqual('b'); + }); + + + it('should handler removing onclick', function() { + let log = ''; + button.onclick = function() { log += 'a'; }; + button.onclick = null as any; + + button.click(); + expect(log).toEqual(''); + }); + + it('should be able to deregister the same event twice', function() { + const listener = (event: Event) => {}; + document.body.addEventListener('click', listener, false); + document.body.removeEventListener('click', listener, false); + document.body.removeEventListener('click', listener, false); + }); + }); + + describe('onEvent default behavior', function() { + let checkbox: HTMLInputElement; + beforeEach(function() { + checkbox = document.createElement('input'); + checkbox.type = 'checkbox'; + document.body.appendChild(checkbox); + }); + + afterEach(function() { document.body.removeChild(checkbox); }); + + it('should be possible to prevent default behavior by returning false', function() { + checkbox.onclick = function() { return false; }; + + checkbox.click(); + expect(checkbox.checked).toBe(false); + }); + + it('should have no effect on default behavior when not returning anything', function() { + checkbox.onclick = function() {}; + + checkbox.click(); + expect(checkbox.checked).toBe(true); + }); + }); +}); diff --git a/packages/zone.js/test/browser/geolocation.spec.manual.ts b/packages/zone.js/test/browser/geolocation.spec.manual.ts new file mode 100644 index 0000000000..17e1c6a5ad --- /dev/null +++ b/packages/zone.js/test/browser/geolocation.spec.manual.ts @@ -0,0 +1,38 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from '../test-util'; + +function supportsGeolocation() { + return 'geolocation' in navigator; +} +(supportsGeolocation).message = 'Geolocation'; + +describe('Geolocation', ifEnvSupports(supportsGeolocation, function() { + const testZone = Zone.current.fork({name: 'geotest'}); + + it('should work for getCurrentPosition', function(done) { + testZone.run(function() { + navigator.geolocation.getCurrentPosition(function(pos) { + expect(Zone.current).toBe(testZone); + done(); + }); + }); + }, 10000); + + it('should work for watchPosition', function(done) { + testZone.run(function() { + let watchId: number; + watchId = navigator.geolocation.watchPosition(function(pos) { + expect(Zone.current).toBe(testZone); + navigator.geolocation.clearWatch(watchId); + done(); + }); + }); + }, 10000); + })); diff --git a/packages/zone.js/test/browser/registerElement.spec.ts b/packages/zone.js/test/browser/registerElement.spec.ts new file mode 100644 index 0000000000..3b3ffdf89c --- /dev/null +++ b/packages/zone.js/test/browser/registerElement.spec.ts @@ -0,0 +1,164 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/* + * check that document.registerElement(name, { prototype: proto }); + * is properly patched + */ + +import {ifEnvSupports} from '../test-util'; + +function registerElement() { + return ('registerElement' in document) && (typeof customElements === 'undefined'); +} +(registerElement).message = 'document.registerElement'; + +describe( + 'document.registerElement', ifEnvSupports(registerElement, function() { + // register a custom element for each callback + const callbackNames = ['created', 'attached', 'detached', 'attributeChanged']; + const callbacks: any = {}; + const testZone = Zone.current.fork({name: 'test'}); + let customElements; + + customElements = testZone.run(function() { + callbackNames.forEach(function(callbackName) { + const fullCallbackName = callbackName + 'Callback'; + const proto = Object.create(HTMLElement.prototype); + (proto as any)[fullCallbackName] = function(arg: any) { callbacks[callbackName](arg); }; + (document).registerElement('x-' + callbackName.toLowerCase(), {prototype: proto}); + }); + }); + + it('should work with createdCallback', function(done) { + callbacks.created = function() { + expect(Zone.current).toBe(testZone); + done(); + }; + + document.createElement('x-created'); + }); + + + it('should work with attachedCallback', function(done) { + callbacks.attached = function() { + expect(Zone.current).toBe(testZone); + done(); + }; + + const elt = document.createElement('x-attached'); + document.body.appendChild(elt); + document.body.removeChild(elt); + }); + + + it('should work with detachedCallback', function(done) { + callbacks.detached = function() { + expect(Zone.current).toBe(testZone); + done(); + }; + + const elt = document.createElement('x-detached'); + document.body.appendChild(elt); + document.body.removeChild(elt); + }); + + + it('should work with attributeChanged', function(done) { + callbacks.attributeChanged = function() { + expect(Zone.current).toBe(testZone); + done(); + }; + + const elt = document.createElement('x-attributechanged'); + elt.id = 'bar'; + }); + + + it('should work with non-writable, non-configurable prototypes created with defineProperty', + function(done) { + testZone.run(function() { + const proto = Object.create(HTMLElement.prototype); + + Object.defineProperty( + proto, 'createdCallback', + {writable: false, configurable: false, value: checkZone}); + + (document).registerElement('x-prop-desc', {prototype: proto}); + + function checkZone() { + expect(Zone.current).toBe(testZone); + done(); + } + }); + + const elt = document.createElement('x-prop-desc'); + }); + + + it('should work with non-writable, non-configurable prototypes created with defineProperties', + function(done) { + testZone.run(function() { + const proto = Object.create(HTMLElement.prototype); + + Object.defineProperties( + proto, + {createdCallback: {writable: false, configurable: false, value: checkZone}}); + + (document).registerElement('x-props-desc', {prototype: proto}); + + function checkZone() { + expect(Zone.current).toBe(testZone); + done(); + } + }); + + const elt = document.createElement('x-props-desc'); + }); + + it('should not throw with frozen prototypes ', function() { + testZone.run(function() { + const proto = Object.create(HTMLElement.prototype, Object.freeze({ + createdCallback: + {value: () => {}, writable: true, configurable: true} + })); + + Object.defineProperty( + proto, 'createdCallback', {writable: false, configurable: false}); + + expect(function() { + (document).registerElement('x-frozen-desc', {prototype: proto}); + }).not.toThrow(); + }); + }); + + + it('should check bind callback if not own property', function(done) { + testZone.run(function() { + const originalProto = {createdCallback: checkZone}; + + const secondaryProto = Object.create(originalProto); + expect(secondaryProto.createdCallback).toBe(originalProto.createdCallback); + + (document).registerElement('x-inherited-callback', {prototype: secondaryProto}); + expect(secondaryProto.createdCallback).not.toBe(originalProto.createdCallback); + + function checkZone() { + expect(Zone.current).toBe(testZone); + done(); + } + + const elt = document.createElement('x-inherited-callback'); + }); + }); + + + it('should not throw if no options passed to registerElement', function() { + expect(function() { (document).registerElement('x-no-opts'); }).not.toThrow(); + }); + })); diff --git a/packages/zone.js/test/browser/requestAnimationFrame.spec.ts b/packages/zone.js/test/browser/requestAnimationFrame.spec.ts new file mode 100644 index 0000000000..9c7788c3e6 --- /dev/null +++ b/packages/zone.js/test/browser/requestAnimationFrame.spec.ts @@ -0,0 +1,52 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from '../test-util'; +declare const window: any; + +describe('requestAnimationFrame', function() { + const functions = + ['requestAnimationFrame', 'webkitRequestAnimationFrame', 'mozRequestAnimationFrame']; + + functions.forEach(function(fnName) { + describe(fnName, ifEnvSupports(fnName, function() { + const originalTimeout: number = (jasmine).DEFAULT_TIMEOUT_INTERVAL; + beforeEach(() => { (jasmine).DEFAULT_TIMEOUT_INTERVAL = 10000; }); + + afterEach(() => { (jasmine).DEFAULT_TIMEOUT_INTERVAL = originalTimeout; }); + const rAF = window[fnName]; + + it('should be tolerant of invalid arguments', function() { + // rAF throws an error on invalid arguments, so expect that. + expect(function() { rAF(null); }).toThrow(); + }); + + it('should bind to same zone when called recursively', function(done) { + Zone.current.fork({name: 'TestZone'}).run(() => { + let frames = 0; + let previousTimeStamp = 0; + + function frameCallback(timestamp: number) { + expect(timestamp).toMatch(/^[\d.]+$/); + // expect previous <= current + expect(previousTimeStamp).not.toBeGreaterThan(timestamp); + previousTimeStamp = timestamp; + + if (frames++ > 15) { + (jasmine).DEFAULT_TIMEOUT_INTERVAL = originalTimeout; + return done(); + } + rAF(frameCallback); + } + + rAF(frameCallback); + }); + }); + })); + }); +}); diff --git a/packages/zone.js/test/browser_entry_point.ts b/packages/zone.js/test/browser_entry_point.ts new file mode 100644 index 0000000000..1057086883 --- /dev/null +++ b/packages/zone.js/test/browser_entry_point.ts @@ -0,0 +1,30 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import '../lib/common/error-rewrite'; + +// import 'core-js/features/set'; +// import 'core-js/features/map'; +// List all tests here: +import './common_tests'; +import './browser/browser.spec'; +import './browser/define-property.spec'; +import './browser/element.spec'; +import './browser/FileReader.spec'; +// import './browser/geolocation.spec.manual'; +import './browser/HTMLImports.spec'; +import './browser/MutationObserver.spec'; +import './browser/registerElement.spec'; +import './browser/requestAnimationFrame.spec'; +import './browser/WebSocket.spec'; +import './browser/XMLHttpRequest.spec'; +import './browser/MediaQuery.spec'; +import './browser/Notification.spec'; +import './browser/Worker.spec'; +import './mocha-patch.spec'; +import './jasmine-patch.spec'; +import './extra/cordova.spec'; diff --git a/packages/zone.js/test/browser_es2015_entry_point.ts b/packages/zone.js/test/browser_es2015_entry_point.ts new file mode 100644 index 0000000000..d66e77c97f --- /dev/null +++ b/packages/zone.js/test/browser_es2015_entry_point.ts @@ -0,0 +1,9 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import './browser/custom-element.spec'; diff --git a/packages/zone.js/test/browser_symbol_setup.ts b/packages/zone.js/test/browser_symbol_setup.ts new file mode 100644 index 0000000000..81cbcc1638 --- /dev/null +++ b/packages/zone.js/test/browser_symbol_setup.ts @@ -0,0 +1,3 @@ +(window as any).global = window; +// Change default symbol prefix for testing to ensure no hard-coded references. +(window as any)['__Zone_symbol_prefix'] = '_test__'; diff --git a/packages/zone.js/test/closure/zone.closure.ts b/packages/zone.js/test/closure/zone.closure.ts new file mode 100644 index 0000000000..00d0d42143 --- /dev/null +++ b/packages/zone.js/test/closure/zone.closure.ts @@ -0,0 +1,130 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import '../../dist/zone-node'; +const testClosureFunction = () => { + const logs: string[] = []; + // call all Zone exposed functions + const testZoneSpec: ZoneSpec = { + name: 'closure', + properties: {}, + onFork: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + zoneSpec: ZoneSpec) => { return parentZoneDelegate.fork(targetZone, zoneSpec); }, + + onIntercept: + (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function, + source: string) => { return parentZoneDelegate.intercept(targetZone, delegate, source); }, + + onInvoke: function( + parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, delegate: Function, + applyThis?: any, applyArgs?: any[], source?: string) { + return parentZoneDelegate.invoke(targetZone, delegate, applyThis, applyArgs, source); + }, + + onHandleError: function( + parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: any) { + return parentZoneDelegate.handleError(targetZone, error); + }, + + onScheduleTask: function( + parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) { + return parentZoneDelegate.scheduleTask(targetZone, task); + }, + + onInvokeTask: function( + parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task, + applyThis?: any, applyArgs?: any[]) { + return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs); + }, + + onCancelTask: function( + parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) { + return parentZoneDelegate.cancelTask(targetZone, task); + }, + + onHasTask: function( + delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) { + return delegate.hasTask(target, hasTaskState); + } + }; + + const testZone: Zone = Zone.current.fork(testZoneSpec); + testZone.runGuarded(() => { + testZone.run(() => { + const properties = testZoneSpec.properties; + properties !['key'] = 'value'; + const keyZone = Zone.current.getZoneWith('key'); + + logs.push('current' + Zone.current.name); + logs.push('parent' + Zone.current.parent !.name); + logs.push('getZoneWith' + keyZone !.name); + logs.push('get' + keyZone !.get('key')); + logs.push('root' + Zone.root.name); + Object.keys((Zone as any).prototype).forEach(key => { logs.push(key); }); + Object.keys(testZoneSpec).forEach(key => { logs.push(key); }); + + const task = Zone.current.scheduleMicroTask('testTask', () => {}, undefined, () => {}); + Object.keys(task).forEach(key => { logs.push(key); }); + }); + }); + + const expectedResult = [ + 'currentclosure', + 'parent', + 'getZoneWithclosure', + 'getvalue', + 'root', + 'parent', + 'name', + 'get', + 'getZoneWith', + 'fork', + 'wrap', + 'run', + 'runGuarded', + 'runTask', + 'scheduleTask', + 'scheduleMicroTask', + 'scheduleMacroTask', + 'scheduleEventTask', + 'cancelTask', + '_updateTaskCount', + 'name', + 'properties', + 'onFork', + 'onIntercept', + 'onInvoke', + 'onHandleError', + 'onScheduleTask', + 'onInvokeTask', + 'onCancelTask', + 'onHasTask', + '_zone', + 'runCount', + '_zoneDelegates', + '_state', + 'type', + 'source', + 'data', + 'scheduleFn', + 'cancelFn', + 'callback', + 'invoke' + ]; + + let result: boolean = true; + for (let i = 0; i < expectedResult.length; i++) { + if (expectedResult[i] !== logs[i]) { + console.log('Not Equals', expectedResult[i], logs[i]); + result = false; + } + } + process['exit'](result ? 0 : 1); +}; +process['on']('uncaughtException', (err: any) => { process['exit'](1); }); + +testClosureFunction(); diff --git a/packages/zone.js/test/common/Error.spec.ts b/packages/zone.js/test/common/Error.spec.ts new file mode 100644 index 0000000000..656cba717f --- /dev/null +++ b/packages/zone.js/test/common/Error.spec.ts @@ -0,0 +1,425 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {isBrowser} from '../../lib/common/utils'; +import {isSafari, zoneSymbol} from '../test-util'; + +// simulate @angular/facade/src/error.ts +class BaseError extends Error { + /** @internal **/ + _nativeError: Error; + + constructor(message: string) { + super(message); + const nativeError = new Error(message) as any as Error; + this._nativeError = nativeError; + } + + get message() { return this._nativeError.message; } + set message(message) { this._nativeError.message = message; } + get name() { return this._nativeError.name; } + get stack() { return (this._nativeError as any).stack; } + set stack(value) { (this._nativeError as any).stack = value; } + toString() { return this._nativeError.toString(); } +} + +class WrappedError extends BaseError { + originalError: any; + + constructor(message: string, error: any) { + super(`${message} caused by: ${error instanceof Error ? error.message : error}`); + this.originalError = error; + } + + get stack() { + return ((this.originalError instanceof Error ? this.originalError : this._nativeError) as any) + .stack; + } +} + +class TestError extends WrappedError { + constructor(message: string, error: any) { + super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error); + } + + get message() { return 'test ' + this.originalError.message; } +} + +class TestMessageError extends WrappedError { + constructor(message: string, error: any) { + super(`${message} caused by: ${error instanceof Error ? error.message : error}`, error); + } + + get message() { return 'test ' + this.originalError.message; } + + set message(value) { this.originalError.message = value; } +} + +describe('ZoneAwareError', () => { + // If the environment does not supports stack rewrites, then these tests will fail + // and there is no point in running them. + const _global: any = typeof window !== 'undefined' ? window : global; + let config: any; + const __karma__ = _global.__karma__; + if (typeof __karma__ !== 'undefined') { + config = __karma__ && (__karma__ as any).config; + } else if (typeof process !== 'undefined') { + config = process.env; + } + const policy = (config && config['errorpolicy']) || 'default'; + if (!(Error as any)['stackRewrite'] && policy !== 'disable') return; + + it('should keep error prototype chain correctly', () => { + class MyError extends Error {} + const myError = new MyError(); + expect(myError instanceof Error).toBe(true); + expect(myError instanceof MyError).toBe(true); + expect(myError.stack).not.toBe(undefined); + }); + + it('should instanceof error correctly', () => { + let myError = Error('myError'); + expect(myError instanceof Error).toBe(true); + let myError1 = Error.call(undefined, 'myError'); + expect(myError1 instanceof Error).toBe(true); + let myError2 = Error.call(global, 'myError'); + expect(myError2 instanceof Error).toBe(true); + let myError3 = Error.call({}, 'myError'); + expect(myError3 instanceof Error).toBe(true); + let myError4 = Error.call({test: 'test'}, 'myError'); + expect(myError4 instanceof Error).toBe(true); + }); + + it('should return error itself from constructor', () => { + class MyError1 extends Error { + constructor() { + const err: any = super('MyError1'); + this.message = err.message; + } + } + let myError1 = new MyError1(); + expect(myError1.message).toEqual('MyError1'); + expect(myError1.name).toEqual('Error'); + }); + + it('should return error by calling error directly', () => { + let myError = Error('myError'); + expect(myError.message).toEqual('myError'); + let myError1 = Error.call(undefined, 'myError'); + expect(myError1.message).toEqual('myError'); + let myError2 = Error.call(global, 'myError'); + expect(myError2.message).toEqual('myError'); + let myError3 = Error.call({}, 'myError'); + expect(myError3.message).toEqual('myError'); + }); + + it('should have browser specified property', () => { + let myError = new Error('myError'); + if (Object.prototype.hasOwnProperty.call(Error.prototype, 'description')) { + // in IE, error has description property + expect((myError).description).toEqual('myError'); + } + if (Object.prototype.hasOwnProperty.call(Error.prototype, 'fileName')) { + // in firefox, error has fileName property + expect((myError).fileName).toBeTruthy(); + } + }); + + it('should not use child Error class get/set in ZoneAwareError constructor', () => { + const func = () => { + const error = new BaseError('test'); + expect(error.message).toEqual('test'); + }; + + expect(func).not.toThrow(); + }); + + it('should behave correctly with wrapped error', () => { + const error = new TestError('originalMessage', new Error('error message')); + expect(error.message).toEqual('test error message'); + error.originalError.message = 'new error message'; + expect(error.message).toEqual('test new error message'); + + const error1 = new TestMessageError('originalMessage', new Error('error message')); + expect(error1.message).toEqual('test error message'); + error1.message = 'new error message'; + expect(error1.message).toEqual('test new error message'); + }); + + it('should copy customized NativeError properties to ZoneAwareError', () => { + const spy = jasmine.createSpy('errorCustomFunction'); + const NativeError = (global as any)[(Zone as any).__symbol__('Error')]; + NativeError.customFunction = function(args: any) { spy(args); }; + expect((Error as any)['customProperty']).toBe('customProperty'); + expect(typeof(Error as any)['customFunction']).toBe('function'); + (Error as any)['customFunction']('test'); + expect(spy).toHaveBeenCalledWith('test'); + }); + + it('should always have stack property even without throw', () => { + // in IE, the stack will be undefined without throw + // in ZoneAwareError, we will make stack always be + // there event without throw + const error = new Error('test'); + const errorWithoutNew = Error('test'); + expect(error.stack !.split('\n').length > 0).toBeTruthy(); + expect(errorWithoutNew.stack !.split('\n').length > 0).toBeTruthy(); + }); + + it('should show zone names in stack frames and remove extra frames', () => { + if (policy === 'disable' || !(Error as any)['stackRewrite']) { + return; + } + if (isBrowser && isSafari()) { + return; + } + const rootZone = Zone.root; + const innerZone = rootZone.fork({name: 'InnerZone'}); + + rootZone.run(testFn); + function testFn() { + let outside: any; + let inside: any; + let outsideWithoutNew: any; + let insideWithoutNew: any; + try { + throw new Error('Outside'); + } catch (e) { + outside = e; + } + try { + throw Error('Outside'); + } catch (e) { + outsideWithoutNew = e; + } + innerZone.run(function insideRun() { + try { + throw new Error('Inside'); + } catch (e) { + inside = e; + } + try { + throw Error('Inside'); + } catch (e) { + insideWithoutNew = e; + } + }); + + if (policy === 'lazy') { + outside.stack = outside.zoneAwareStack; + outsideWithoutNew.stack = outsideWithoutNew.zoneAwareStack; + inside.stack = inside.zoneAwareStack; + insideWithoutNew.stack = insideWithoutNew.zoneAwareStack; + } + + expect(outside.stack).toEqual(outside.zoneAwareStack); + expect(outsideWithoutNew.stack).toEqual(outsideWithoutNew.zoneAwareStack); + expect(inside !.stack).toEqual(inside !.zoneAwareStack); + expect(insideWithoutNew !.stack).toEqual(insideWithoutNew !.zoneAwareStack); + expect(typeof inside !.originalStack).toEqual('string'); + expect(typeof insideWithoutNew !.originalStack).toEqual('string'); + const outsideFrames = outside.stack !.split(/\n/); + const insideFrames = inside !.stack !.split(/\n/); + const outsideWithoutNewFrames = outsideWithoutNew !.stack !.split(/\n/); + const insideWithoutNewFrames = insideWithoutNew !.stack !.split(/\n/); + + // throw away first line if it contains the error + if (/Outside/.test(outsideFrames[0])) { + outsideFrames.shift(); + } + if (/Error /.test(outsideFrames[0])) { + outsideFrames.shift(); + } + + if (/Outside/.test(outsideWithoutNewFrames[0])) { + outsideWithoutNewFrames.shift(); + } + if (/Error /.test(outsideWithoutNewFrames[0])) { + outsideWithoutNewFrames.shift(); + } + + if (/Inside/.test(insideFrames[0])) { + insideFrames.shift(); + } + if (/Error /.test(insideFrames[0])) { + insideFrames.shift(); + } + + if (/Inside/.test(insideWithoutNewFrames[0])) { + insideWithoutNewFrames.shift(); + } + if (/Error /.test(insideWithoutNewFrames[0])) { + insideWithoutNewFrames.shift(); + } + expect(outsideFrames[0]).toMatch(/testFn.*[]/); + + expect(insideFrames[0]).toMatch(/insideRun.*[InnerZone]]/); + expect(insideFrames[1]).toMatch(/testFn.*[]]/); + + expect(outsideWithoutNewFrames[0]).toMatch(/testFn.*[]/); + + expect(insideWithoutNewFrames[0]).toMatch(/insideRun.*[InnerZone]]/); + expect(insideWithoutNewFrames[1]).toMatch(/testFn.*[]]/); + } + }); + + const zoneAwareFrames = [ + 'Zone.run', 'Zone.runGuarded', 'Zone.scheduleEventTask', 'Zone.scheduleMicroTask', + 'Zone.scheduleMacroTask', 'Zone.runTask', 'ZoneDelegate.scheduleTask', + 'ZoneDelegate.invokeTask', 'zoneAwareAddListener', 'Zone.prototype.run', + 'Zone.prototype.runGuarded', 'Zone.prototype.scheduleEventTask', + 'Zone.prototype.scheduleMicroTask', 'Zone.prototype.scheduleMacroTask', + 'Zone.prototype.runTask', 'ZoneDelegate.prototype.scheduleTask', + 'ZoneDelegate.prototype.invokeTask', 'ZoneTask.invokeTask' + ]; + + function assertStackDoesNotContainZoneFrames(err: any) { + const frames = policy === 'lazy' ? err.zoneAwareStack.split('\n') : err.stack.split('\n'); + if (policy === 'disable') { + let hasZoneStack = false; + for (let i = 0; i < frames.length; i++) { + if (hasZoneStack) { + break; + } + hasZoneStack = zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1).length > 0; + } + if (!hasZoneStack) { + console.log('stack', err.originalStack); + } + expect(hasZoneStack).toBe(true); + } else { + for (let i = 0; i < frames.length; i++) { + expect(zoneAwareFrames.filter(f => frames[i].indexOf(f) !== -1)).toEqual([]); + } + } + }; + + const errorZoneSpec = { + name: 'errorZone', + done: <(() => void)|null>null, + onHandleError: + (parentDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => { + assertStackDoesNotContainZoneFrames(error); + setTimeout(() => { errorZoneSpec.done && errorZoneSpec.done(); }, 0); + return false; + } + }; + + const errorZone = Zone.root.fork(errorZoneSpec); + + const assertStackDoesNotContainZoneFramesTest = function(testFn: Function) { + return function(done: () => void) { + errorZoneSpec.done = done; + errorZone.run(testFn); + }; + }; + + describe('Error stack', () => { + it('Error with new which occurs in setTimeout callback should not have zone frames visible', + assertStackDoesNotContainZoneFramesTest( + () => { setTimeout(() => { throw new Error('timeout test error'); }, 10); })); + + it('Error without new which occurs in setTimeout callback should not have zone frames visible', + assertStackDoesNotContainZoneFramesTest( + () => { setTimeout(() => { throw Error('test error'); }, 10); })); + + it('Error with new which cause by promise rejection should not have zone frames visible', + (done) => { + const p = new Promise( + (resolve, reject) => { setTimeout(() => { reject(new Error('test error')); }); }); + p.catch(err => { + assertStackDoesNotContainZoneFrames(err); + done(); + }); + }); + + it('Error without new which cause by promise rejection should not have zone frames visible', + (done) => { + const p = new Promise( + (resolve, reject) => { setTimeout(() => { reject(Error('test error')); }); }); + p.catch(err => { + assertStackDoesNotContainZoneFrames(err); + done(); + }); + }); + + it('Error with new which occurs in eventTask callback should not have zone frames visible', + assertStackDoesNotContainZoneFramesTest(() => { + const task = Zone.current.scheduleEventTask('errorEvent', () => { + throw new Error('test error'); + }, undefined, () => null, undefined); + task.invoke(); + })); + + it('Error without new which occurs in eventTask callback should not have zone frames visible', + assertStackDoesNotContainZoneFramesTest(() => { + const task = Zone.current.scheduleEventTask( + 'errorEvent', () => { throw Error('test error'); }, undefined, () => null, undefined); + task.invoke(); + })); + + it('Error with new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible', + assertStackDoesNotContainZoneFramesTest(() => { + const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec']) + .scheduleEventTask('errorEvent', () => { + throw new Error('test error'); + }, undefined, () => null, undefined); + task.invoke(); + })); + + it('Error without new which occurs in longStackTraceZone should not have zone frames and longStackTraceZone frames visible', + assertStackDoesNotContainZoneFramesTest(() => { + const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec']) + .scheduleEventTask('errorEvent', () => { + throw Error('test error'); + }, undefined, () => null, undefined); + task.invoke(); + })); + + it('stack frames of the callback in user customized zoneSpec should be kept', + assertStackDoesNotContainZoneFramesTest(() => { + const task = Zone.current.fork((Zone as any)['longStackTraceZoneSpec']) + .fork({ + name: 'customZone', + onScheduleTask: (parentDelegate, currentZone, targetZone, task) => { + return parentDelegate.scheduleTask(targetZone, task); + }, + onHandleError: (parentDelegate, currentZone, targetZone, error) => { + parentDelegate.handleError(targetZone, error); + const containsCustomZoneSpecStackTrace = + error.stack.indexOf('onScheduleTask') !== -1; + expect(containsCustomZoneSpecStackTrace).toBeTruthy(); + return false; + } + }) + .scheduleEventTask('errorEvent', () => { + throw new Error('test error'); + }, undefined, () => null, undefined); + task.invoke(); + })); + + it('should be able to generate zone free stack even NativeError stack is readonly', function() { + const _global: any = + typeof window === 'object' && window || typeof self === 'object' && self || global; + const NativeError = _global[zoneSymbol('Error')]; + const desc = Object.getOwnPropertyDescriptor(NativeError.prototype, 'stack'); + if (desc) { + const originalSet: ((value: any) => void)|undefined = desc.set; + // make stack readonly + desc.set = null as any; + + try { + const error = new Error('test error'); + expect(error.stack).toBeTruthy(); + assertStackDoesNotContainZoneFrames(error); + } finally { + desc.set = originalSet; + } + } + }); + }); +}); diff --git a/packages/zone.js/test/common/Promise.spec.ts b/packages/zone.js/test/common/Promise.spec.ts new file mode 100644 index 0000000000..7165b184be --- /dev/null +++ b/packages/zone.js/test/common/Promise.spec.ts @@ -0,0 +1,521 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {isNode, zoneSymbol} from '../../lib/common/utils'; +import {ifEnvSupports} from '../test-util'; + +declare const global: any; + +class MicroTaskQueueZoneSpec implements ZoneSpec { + name: string = 'MicroTaskQueue'; + queue: MicroTask[] = []; + properties = {queue: this.queue, flush: this.flush.bind(this)}; + + flush() { + while (this.queue.length) { + const task = this.queue.shift(); + task !.invoke(); + } + } + + onScheduleTask(delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): any { + this.queue.push(task as MicroTask); + } +} + +function flushMicrotasks() { + Zone.current.get('flush')(); +} + +class TestRejection { + prop1?: string; + prop2?: string; +} + +describe( + 'Promise', ifEnvSupports('Promise', function() { + if (!global.Promise) return; + let log: string[]; + let queueZone: Zone; + let testZone: Zone; + let pZone: Zone; + + beforeEach(() => { + testZone = Zone.current.fork({name: 'TestZone'}); + + pZone = Zone.current.fork({ + name: 'promise-zone', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): any => { + log.push('scheduleTask'); + parentZoneDelegate.scheduleTask(targetZone, task); + } + }); + + queueZone = Zone.current.fork(new MicroTaskQueueZoneSpec()); + + log = []; + }); + + xit('should allow set es6 Promise after load ZoneAwarePromise', (done) => { + const ES6Promise = require('es6-promise').Promise; + const NativePromise = global[zoneSymbol('Promise')]; + + try { + global['Promise'] = ES6Promise; + Zone.assertZonePatched(); + expect(global[zoneSymbol('Promise')]).toBe(ES6Promise); + const promise = Promise.resolve(0); + console.log('promise', promise); + promise + .then(value => { + expect(value).toBe(0); + done(); + }) + .catch(error => { fail(error); }); + } finally { + global['Promise'] = NativePromise; + Zone.assertZonePatched(); + expect(global[zoneSymbol('Promise')]).toBe(NativePromise); + } + }); + + it('should pretend to be a native code', + () => { expect(String(Promise).indexOf('[native code]') >= 0).toBe(true); }); + + it('should use native toString for promise instance', () => { + expect(Object.prototype.toString.call(Promise.resolve())).toEqual('[object Promise]'); + }); + + it('should make sure that new Promise is instance of Promise', () => { + expect(Promise.resolve(123) instanceof Promise).toBe(true); + expect(new Promise(() => null) instanceof Promise).toBe(true); + }); + + xit('should ensure that Promise this is instanceof Promise', () => { + expect(() => { + Promise.call({}, () => null); + }).toThrowError('Must be an instanceof Promise.'); + }); + + xit('should allow subclassing', () => { + class MyPromise extends Promise { + constructor(fn: any) { super(fn); } + } + expect(new MyPromise(null).then(() => null) instanceof MyPromise).toBe(true); + }); + + it('should intercept scheduling of resolution and then', (done) => { + pZone.run(() => { + let p: Promise = + new Promise(function(resolve, reject) { expect(resolve('RValue')).toBe(undefined); }); + expect(log).toEqual([]); + expect(p instanceof Promise).toBe(true); + p = p.then((v) => { + log.push(v); + expect(v).toBe('RValue'); + expect(log).toEqual(['scheduleTask', 'RValue']); + return 'second value'; + }); + expect(p instanceof Promise).toBe(true); + expect(log).toEqual(['scheduleTask']); + p = p.then((v) => { + log.push(v); + expect(log).toEqual(['scheduleTask', 'RValue', 'scheduleTask', 'second value']); + done(); + }); + expect(p instanceof Promise).toBe(true); + expect(log).toEqual(['scheduleTask']); + }); + }); + + it('should allow sync resolution of promises', () => { + queueZone.run(() => { + const flush = Zone.current.get('flush'); + const queue = Zone.current.get('queue'); + const p = new Promise(function(resolve, reject) { resolve('RValue'); }) + .then((v: string) => { + log.push(v); + return 'second value'; + }) + .then((v: string) => { log.push(v); }); + expect(queue.length).toEqual(1); + expect(log).toEqual([]); + flush(); + expect(log).toEqual(['RValue', 'second value']); + }); + }); + + it('should allow sync resolution of promises returning promises', () => { + queueZone.run(() => { + const flush = Zone.current.get('flush'); + const queue = Zone.current.get('queue'); + const p = + new Promise(function(resolve, reject) { resolve(Promise.resolve('RValue')); }) + .then((v: string) => { + log.push(v); + return Promise.resolve('second value'); + }) + .then((v: string) => { log.push(v); }); + expect(queue.length).toEqual(1); + expect(log).toEqual([]); + flush(); + expect(log).toEqual(['RValue', 'second value']); + }); + }); + + describe('Promise API', function() { + it('should work with .then', function(done) { + let resolve: Function|null = null; + + testZone.run(function() { + new Promise(function(resolveFn) { resolve = resolveFn; }).then(function() { + expect(Zone.current).toBe(testZone); + done(); + }); + }); + + resolve !(); + }); + + it('should work with .catch', function(done) { + let reject: (() => void)|null = null; + + testZone.run(function() { + new Promise(function(resolveFn, rejectFn) { reject = rejectFn; })['catch'](function() { + expect(Zone.current).toBe(testZone); + done(); + }); + }); + + + expect(reject !()).toBe(undefined); + }); + + it('should work with .finally with resolved promise', function(done) { + let resolve: Function|null = null; + + testZone.run(function() { + (new Promise(function(resolveFn) { resolve = resolveFn; }) as any).finally(function() { + expect(arguments.length).toBe(0); + expect(Zone.current).toBe(testZone); + done(); + }); + }); + + resolve !('value'); + }); + + it('should work with .finally with rejected promise', function(done) { + let reject: Function|null = null; + + testZone.run(function() { + (new Promise(function(_, rejectFn) { reject = rejectFn; }) as any).finally(function() { + expect(arguments.length).toBe(0); + expect(Zone.current).toBe(testZone); + done(); + }); + }); + + reject !('error'); + }); + + it('should work with Promise.resolve', () => { + queueZone.run(() => { + let value: any = null; + Promise.resolve('resolveValue').then((v) => value = v); + expect(Zone.current.get('queue').length).toEqual(1); + flushMicrotasks(); + expect(value).toEqual('resolveValue'); + }); + }); + + it('should work with Promise.reject', () => { + queueZone.run(() => { + let value: any = null; + Promise.reject('rejectReason')['catch']((v) => value = v); + expect(Zone.current.get('queue').length).toEqual(1); + flushMicrotasks(); + expect(value).toEqual('rejectReason'); + }); + }); + + describe('reject', () => { + it('should reject promise', () => { + queueZone.run(() => { + let value: any = null; + Promise.reject('rejectReason')['catch']((v) => value = v); + flushMicrotasks(); + expect(value).toEqual('rejectReason'); + }); + }); + + it('should re-reject promise', () => { + queueZone.run(() => { + let value: any = null; + Promise.reject('rejectReason')['catch']((v) => { throw v; })['catch']( + (v) => value = v); + flushMicrotasks(); + expect(value).toEqual('rejectReason'); + }); + }); + + it('should reject and recover promise', () => { + queueZone.run(() => { + let value: any = null; + Promise.reject('rejectReason')['catch']((v) => v).then((v) => value = v); + flushMicrotasks(); + expect(value).toEqual('rejectReason'); + }); + }); + + it('should reject if chained promise does not catch promise', () => { + queueZone.run(() => { + let value: any = null; + Promise.reject('rejectReason') + .then((v) => fail('should not get here')) + .then(null, (v) => value = v); + flushMicrotasks(); + expect(value).toEqual('rejectReason'); + }); + }); + + it('should output error to console if ignoreConsoleErrorUncaughtError is false', + (done) => { + Zone.current.fork({name: 'promise-error'}).run(() => { + (Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = false; + const originalConsoleError = console.error; + console.error = jasmine.createSpy('consoleErr'); + const p = new Promise((resolve, reject) => { throw new Error('promise error'); }); + setTimeout(() => { + expect(console.error).toHaveBeenCalled(); + console.error = originalConsoleError; + done(); + }, 10); + }); + }); + + it('should not output error to console if ignoreConsoleErrorUncaughtError is true', + (done) => { + Zone.current.fork({name: 'promise-error'}).run(() => { + (Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = true; + const originalConsoleError = console.error; + console.error = jasmine.createSpy('consoleErr'); + const p = new Promise((resolve, reject) => { throw new Error('promise error'); }); + setTimeout(() => { + expect(console.error).not.toHaveBeenCalled(); + console.error = originalConsoleError; + (Zone as any)[Zone.__symbol__('ignoreConsoleErrorUncaughtError')] = false; + done(); + }, 10); + }); + }); + + it('should notify Zone.onHandleError if no one catches promise', (done) => { + let promiseError: Error|null = null; + let zone: Zone|null = null; + let task: Task|null = null; + let error: Error|null = null; + queueZone + .fork({ + name: 'promise-error', + onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any): + boolean => { + promiseError = error; + delegate.handleError(target, error); + return false; + } + }) + .run(() => { + zone = Zone.current; + task = Zone.currentTask; + error = new Error('rejectedErrorShouldBeHandled'); + try { + // throw so that the stack trace is captured + throw error; + } catch (e) { + } + Promise.reject(error); + expect(promiseError).toBe(null); + }); + setTimeout((): any => null); + setTimeout(() => { + expect(promiseError !.message) + .toBe( + 'Uncaught (in promise): ' + error + + (error !.stack ? '\n' + error !.stack : '')); + expect((promiseError as any)['rejection']).toBe(error); + expect((promiseError as any)['zone']).toBe(zone); + expect((promiseError as any)['task']).toBe(task); + done(); + }); + }); + + it('should print readable information when throw a not error object', (done) => { + let promiseError: Error|null = null; + let zone: Zone|null = null; + let task: Task|null = null; + let rejectObj: TestRejection; + queueZone + .fork({ + name: 'promise-error', + onHandleError: (delegate: ZoneDelegate, current: Zone, target: Zone, error: any): + boolean => { + promiseError = error; + delegate.handleError(target, error); + return false; + } + }) + .run(() => { + zone = Zone.current; + task = Zone.currentTask; + rejectObj = new TestRejection(); + rejectObj.prop1 = 'value1'; + rejectObj.prop2 = 'value2'; + Promise.reject(rejectObj); + expect(promiseError).toBe(null); + }); + setTimeout((): any => null); + setTimeout(() => { + expect(promiseError !.message) + .toMatch(/Uncaught \(in promise\):.*: {"prop1":"value1","prop2":"value2"}/); + done(); + }); + }); + }); + + describe('Promise.race', () => { + it('should reject the value', () => { + queueZone.run(() => { + let value: any = null; + (Promise as any).race([ + Promise.reject('rejection1'), 'v1' + ])['catch']((v: any) => value = v); + // expect(Zone.current.get('queue').length).toEqual(2); + flushMicrotasks(); + expect(value).toEqual('rejection1'); + }); + }); + + it('should resolve the value', () => { + queueZone.run(() => { + let value: any = null; + (Promise as any) + .race([Promise.resolve('resolution'), 'v1']) + .then((v: any) => value = v); + // expect(Zone.current.get('queue').length).toEqual(2); + flushMicrotasks(); + expect(value).toEqual('resolution'); + }); + }); + }); + + describe('Promise.all', () => { + it('should reject the value', () => { + queueZone.run(() => { + let value: any = null; + Promise.all([Promise.reject('rejection'), 'v1'])['catch']((v: any) => value = v); + // expect(Zone.current.get('queue').length).toEqual(2); + flushMicrotasks(); + expect(value).toEqual('rejection'); + }); + }); + + it('should resolve the value', () => { + queueZone.run(() => { + let value: any = null; + Promise.all([Promise.resolve('resolution'), 'v1']).then((v: any) => value = v); + // expect(Zone.current.get('queue').length).toEqual(2); + flushMicrotasks(); + expect(value).toEqual(['resolution', 'v1']); + }); + }); + + it('should resolve with the sync then operation', () => { + queueZone.run(() => { + let value: any = null; + const p1 = {then: function(thenCallback: Function) { return thenCallback('p1'); }}; + const p2 = {then: function(thenCallback: Function) { return thenCallback('p2'); }}; + Promise.all([p1, 'v1', p2]).then((v: any) => value = v); + // expect(Zone.current.get('queue').length).toEqual(2); + flushMicrotasks(); + expect(value).toEqual(['p1', 'v1', 'p2']); + }); + }); + + it('should resolve generators', + ifEnvSupports( + () => { return isNode; }, + () => { + const generators: any = function* () { + yield Promise.resolve(1); + yield Promise.resolve(2); + return; + }; + queueZone.run(() => { + let value: any = null; + Promise.all(generators()).then(val => { value = val; }); + // expect(Zone.current.get('queue').length).toEqual(2); + flushMicrotasks(); + expect(value).toEqual([1, 2]); + }); + })); + }); + }); + + describe('Promise subclasses', function() { + class MyPromise { + private _promise: Promise; + constructor(init: any) { this._promise = new Promise(init); } + + catch(onrejected?: ((reason: any) => TResult | PromiseLike)| + undefined|null): Promise { + return this._promise.catch.call(this._promise, onrejected); + }; + + then( + onfulfilled?: ((value: T) => TResult1 | PromiseLike)|undefined|null, + onrejected?: ((reason: any) => TResult2 | PromiseLike)|undefined| + null): Promise { + return this._promise.then.call(this._promise, onfulfilled, onrejected); + }; + } + + const setPrototypeOf = (Object as any).setPrototypeOf || function(obj: any, proto: any) { + obj.__proto__ = proto; + return obj; + }; + + setPrototypeOf(MyPromise.prototype, Promise.prototype); + + it('should reject if the Promise subclass rejects', function() { + const myPromise = + new MyPromise(function(resolve: any, reject: any): void { reject('foo'); }); + + return Promise.resolve() + .then(function() { return myPromise; }) + .then( + function() { throw new Error('Unexpected resolution'); }, + function(result) { expect(result).toBe('foo'); }); + }); + + function testPromiseSubClass(done?: Function) { + const myPromise = + new MyPromise(function(resolve: any, reject: Function) { resolve('foo'); }); + + return Promise.resolve().then(function() { return myPromise; }).then(function(result) { + expect(result).toBe('foo'); + done && done(); + }); + } + + it('should resolve if the Promise subclass resolves', jasmine ? function(done) { + testPromiseSubClass(done); + } : function() { testPromiseSubClass(); }); + }); + })); diff --git a/packages/zone.js/test/common/fetch.spec.ts b/packages/zone.js/test/common/fetch.spec.ts new file mode 100644 index 0000000000..8df75786cd --- /dev/null +++ b/packages/zone.js/test/common/fetch.spec.ts @@ -0,0 +1,205 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {ifEnvSupports, ifEnvSupportsWithDone, isFirefox, isSafari} from '../test-util'; + +declare const global: any; + +describe( + 'fetch', ifEnvSupports('fetch', function() { + let testZone: Zone; + beforeEach(() => { testZone = Zone.current.fork({name: 'TestZone'}); }); + it('should work for text response', function(done) { + testZone.run(function() { + global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json') + .then(function(response: any) { + const fetchZone = Zone.current; + expect(fetchZone.name).toBe(testZone.name); + + response.text().then(function(text: string) { + expect(Zone.current.name).toBe(fetchZone.name); + expect(text.trim()).toEqual('{"hello": "world"}'); + done(); + }); + }); + }); + }); + + it('should work for json response', function(done) { + testZone.run(function() { + global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json') + .then(function(response: any) { + const fetchZone = Zone.current; + expect(fetchZone.name).toBe(testZone.name); + + response.json().then(function(obj: any) { + expect(Zone.current.name).toBe(fetchZone.name); + expect(obj.hello).toEqual('world'); + done(); + }); + }); + }); + }); + + it('should work for blob response', function(done) { + testZone.run(function() { + global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json') + .then(function(response: any) { + const fetchZone = Zone.current; + expect(fetchZone.name).toBe(testZone.name); + + // Android 4.3- doesn't support response.blob() + if (response.blob) { + response.blob().then(function(blob: any) { + expect(Zone.current.name).toBe(fetchZone.name); + expect(blob instanceof Blob).toEqual(true); + done(); + }); + } else { + done(); + } + }); + }); + }); + + it('should work for arrayBuffer response', function(done) { + testZone.run(function() { + global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json') + .then(function(response: any) { + const fetchZone = Zone.current; + expect(fetchZone.name).toBe(testZone.name); + + // Android 4.3- doesn't support response.arrayBuffer() + if (response.arrayBuffer) { + response.arrayBuffer().then(function(blob: any) { + expect(Zone.current).toBe(fetchZone); + expect(blob instanceof ArrayBuffer).toEqual(true); + done(); + }); + } else { + done(); + } + }); + }); + }); + + it('should throw error when send crendential', + ifEnvSupportsWithDone(isFirefox, function(done: DoneFn) { + testZone.run(function() { + global['fetch']('http://user:password@example.com') + .then( + function(response: any) { fail('should not success'); }, + (error: any) => { + expect(Zone.current.name).toEqual(testZone.name); + expect(error.constructor.name).toEqual('TypeError'); + done(); + }); + }); + })); + + describe('macroTask', () => { + const logs: string[] = []; + let fetchZone: Zone; + let fetchTask: any = null; + beforeEach(() => { + logs.splice(0); + fetchZone = Zone.current.fork({ + name: 'fetch', + onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => { + if (task.type !== 'eventTask') { + logs.push(`scheduleTask:${task.source}:${task.type}`); + } + if (task.source === 'fetch') { + fetchTask = task; + } + return delegate.scheduleTask(target, task); + }, + onInvokeTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task, + applyThis: any, applyArgs: any) => { + if (task.type !== 'eventTask') { + logs.push(`invokeTask:${task.source}:${task.type}`); + } + return delegate.invokeTask(target, task, applyThis, applyArgs); + }, + onCancelTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => { + if (task.type !== 'eventTask') { + logs.push(`cancelTask:${task.source}:${task.type}`); + } + return delegate.cancelTask(target, task); + } + }); + }); + it('fetch should be considered as macroTask', (done: DoneFn) => { + fetchZone.run(() => { + global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json') + .then(function(response: any) { + expect(Zone.current.name).toBe(fetchZone.name); + expect(logs).toEqual([ + 'scheduleTask:fetch:macroTask', 'scheduleTask:Promise.then:microTask', + 'invokeTask:Promise.then:microTask', 'invokeTask:fetch:macroTask', + 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask' + ]); + done(); + }); + }); + }); + + it('cancel fetch should invoke onCancelTask', + ifEnvSupportsWithDone('AbortController', (done: DoneFn) => { + if (isSafari) { + // safari not work with AbortController + done(); + return; + } + fetchZone.run(() => { + const AbortController = global['AbortController']; + const abort = new AbortController(); + const signal = abort.signal; + global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json', {signal}) + .then(function(response: any) { fail('should not get response'); }) + .catch(function(error: any) { + expect(error.name).toEqual('AbortError'); + expect(logs).toEqual([ + 'scheduleTask:fetch:macroTask', 'cancelTask:fetch:macroTask', + 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask', + 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask', + 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask' + ]); + done(); + }); + abort.abort(); + }); + })); + + it('cancel fetchTask should trigger abort', + ifEnvSupportsWithDone('AbortController', (done: DoneFn) => { + if (isSafari) { + // safari not work with AbortController + done(); + return; + } + fetchZone.run(() => { + const AbortController = global['AbortController']; + const abort = new AbortController(); + const signal = abort.signal; + global['fetch']('/base/angular/packages/zone.js/test/assets/sample.json', {signal}) + .then(function(response: any) { fail('should not get response'); }) + .catch(function(error: any) { + expect(error.name).toEqual('AbortError'); + expect(logs).toEqual([ + 'scheduleTask:fetch:macroTask', 'cancelTask:fetch:macroTask', + 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask', + 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask', + 'scheduleTask:Promise.then:microTask', 'invokeTask:Promise.then:microTask' + ]); + done(); + }); + fetchTask.zone.cancelTask(fetchTask); + }); + })); + }); + })); diff --git a/packages/zone.js/test/common/microtasks.spec.ts b/packages/zone.js/test/common/microtasks.spec.ts new file mode 100644 index 0000000000..2e70c78aad --- /dev/null +++ b/packages/zone.js/test/common/microtasks.spec.ts @@ -0,0 +1,100 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +describe('Microtasks', function() { + if (!global.Promise) return; + + function scheduleFn(task: Task) { Promise.resolve().then(task.invoke); } + + it('should execute microtasks enqueued in the root zone', function(done) { + const log: number[] = []; + + Zone.current.scheduleMicroTask('test', () => log.push(1), undefined, scheduleFn); + Zone.current.scheduleMicroTask('test', () => log.push(2), undefined, scheduleFn); + Zone.current.scheduleMicroTask('test', () => log.push(3), undefined, scheduleFn); + + setTimeout(function() { + expect(log).toEqual([1, 2, 3]); + done(); + }, 10); + }); + + it('should correctly scheduleMacroTask microtasks vs macrotasks', function(done) { + const log = ['+root']; + + Zone.current.scheduleMicroTask('test', () => log.push('root.mit'), undefined, scheduleFn); + + setTimeout(function() { + log.push('+mat1'); + Zone.current.scheduleMicroTask('test', () => log.push('mat1.mit'), undefined, scheduleFn); + log.push('-mat1'); + }, 10); + + setTimeout(function() { log.push('mat2'); }, 30); + + setTimeout(function() { + expect(log).toEqual(['+root', '-root', 'root.mit', '+mat1', '-mat1', 'mat1.mit', 'mat2']); + done(); + }, 40); + + log.push('-root'); + }); + + it('should execute Promise wrapCallback in the zone where they are scheduled', function(done) { + const resolvedPromise = Promise.resolve(null); + + const testZone = Zone.current.fork({name: ''}); + + testZone.run(function() { + resolvedPromise.then(function() { + expect(Zone.current.name).toBe(testZone.name); + done(); + }); + }); + }); + + it('should execute Promise wrapCallback in the zone where they are scheduled even if resolved ' + + 'in different zone.', + function(done) { + let resolve: Function; + const promise = new Promise(function(rs) { resolve = rs; }); + + const testZone = Zone.current.fork({name: 'test'}); + + testZone.run(function() { + promise.then(function() { + expect(Zone.current).toBe(testZone); + done(); + }); + }); + + Zone.current.fork({name: 'test'}).run(function() { resolve(null); }); + }); + + describe('Promise', function() { + it('should go through scheduleTask', function(done) { + let called = false; + const testZone = Zone.current.fork({ + name: 'test', + onScheduleTask: function(delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): + Task { + called = true; + delegate.scheduleTask(target, task); + return task; + } + }); + + testZone.run(function() { + Promise.resolve('value').then(function() { + expect(called).toEqual(true); + done(); + }); + }); + }); + }); +}); diff --git a/packages/zone.js/test/common/setInterval.spec.ts b/packages/zone.js/test/common/setInterval.spec.ts new file mode 100644 index 0000000000..cee6d8eabc --- /dev/null +++ b/packages/zone.js/test/common/setInterval.spec.ts @@ -0,0 +1,89 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +'use strict'; +import {isNode, zoneSymbol} from '../../lib/common/utils'; +declare const global: any; +const wtfMock = global.wtfMock; + +describe('setInterval', function() { + it('should work with setInterval', function(done) { + let cancelId: any; + const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'}); + testZone.run(() => { + let intervalCount = 0; + let timeoutRunning = false; + const intervalFn = function() { + intervalCount++; + expect(Zone.current.name).toEqual(('TestZone')); + if (timeoutRunning) { + return; + } + timeoutRunning = true; + global[zoneSymbol('setTimeout')](function() { + const intervalUnitLog = [ + '> Zone:invokeTask:setInterval("::ProxyZone::WTF::TestZone")', + '< Zone:invokeTask:setInterval' + ]; + let intervalLog: string[] = []; + for (let i = 0; i < intervalCount; i++) { + intervalLog = intervalLog.concat(intervalUnitLog); + } + expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")'); + expect(wtfMock.log[1]) + .toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); + expect(wtfMock.log[2]) + .toContain( + '# Zone:schedule:macroTask:setInterval("::ProxyZone::WTF::TestZone"'); + expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test'); + expect(wtfMock.log.splice(4)).toEqual(intervalLog); + clearInterval(cancelId); + done(); + }); + }; + expect(Zone.current.name).toEqual(('TestZone')); + cancelId = setInterval(intervalFn, 10); + if (isNode) { + expect(typeof cancelId.ref).toEqual(('function')); + expect(typeof cancelId.unref).toEqual(('function')); + } + + expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")'); + expect(wtfMock.log[1]).toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); + expect(wtfMock.log[2]) + .toContain('# Zone:schedule:macroTask:setInterval("::ProxyZone::WTF::TestZone"'); + }, null, undefined, 'unit-test'); + }); + + it('should not cancel the task after invoke the setInterval callback', (done) => { + const logs: HasTaskState[] = []; + const zone = Zone.current.fork({ + name: 'interval', + onHasTask: + (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, hasTask: HasTaskState) => { + logs.push(hasTask); + return delegate.hasTask(targetZone, hasTask); + } + }); + + zone.run(() => { + const timerId = setInterval(() => {}, 100); + (global as any)[Zone.__symbol__('setTimeout')](() => { + expect(logs.length > 0).toBeTruthy(); + expect(logs).toEqual( + [{microTask: false, macroTask: true, eventTask: false, change: 'macroTask'}]); + clearInterval(timerId); + expect(logs).toEqual([ + {microTask: false, macroTask: true, eventTask: false, change: 'macroTask'}, + {microTask: false, macroTask: false, eventTask: false, change: 'macroTask'} + ]); + done(); + }, 300); + }); + }); +}); diff --git a/packages/zone.js/test/common/setTimeout.spec.ts b/packages/zone.js/test/common/setTimeout.spec.ts new file mode 100644 index 0000000000..8c1f7c6789 --- /dev/null +++ b/packages/zone.js/test/common/setTimeout.spec.ts @@ -0,0 +1,123 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {isNode, zoneSymbol} from '../../lib/common/utils'; +declare const global: any; +const wtfMock = global.wtfMock; + +describe('setTimeout', function() { + it('should intercept setTimeout', function(done) { + let cancelId: any; + const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'}); + testZone.run(() => { + const timeoutFn = function() { + expect(Zone.current.name).toEqual(('TestZone')); + global[zoneSymbol('setTimeout')](function() { + expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")'); + expect(wtfMock.log[1]) + .toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); + expect(wtfMock.log[2]) + .toContain('# Zone:schedule:macroTask:setTimeout("::ProxyZone::WTF::TestZone"'); + expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test'); + expect(wtfMock.log[4]) + .toEqual('> Zone:invokeTask:setTimeout("::ProxyZone::WTF::TestZone")'); + expect(wtfMock.log[5]).toEqual('< Zone:invokeTask:setTimeout'); + done(); + }); + }; + expect(Zone.current.name).toEqual(('TestZone')); + cancelId = setTimeout(timeoutFn, 3); + if (isNode) { + expect(typeof cancelId.ref).toEqual(('function')); + expect(typeof cancelId.unref).toEqual(('function')); + } + expect(wtfMock.log[0]).toEqual('# Zone:fork("::ProxyZone::WTF", "TestZone")'); + expect(wtfMock.log[1]).toEqual('> Zone:invoke:unit-test("::ProxyZone::WTF::TestZone")'); + expect(wtfMock.log[2]) + .toContain('# Zone:schedule:macroTask:setTimeout("::ProxyZone::WTF::TestZone"'); + }, null, undefined, 'unit-test'); + }); + + it('should allow canceling of fns registered with setTimeout', function(done) { + const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'}); + testZone.run(() => { + const spy = jasmine.createSpy('spy'); + const cancelId = setTimeout(spy, 0); + clearTimeout(cancelId); + setTimeout(function() { + expect(spy).not.toHaveBeenCalled(); + done(); + }, 1); + }); + }); + + it('should allow cancelation of fns registered with setTimeout after invocation', function(done) { + const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'}); + testZone.run(() => { + const spy = jasmine.createSpy('spy'); + const cancelId = setTimeout(spy, 0); + setTimeout(function() { + expect(spy).toHaveBeenCalled(); + setTimeout(function() { + clearTimeout(cancelId); + done(); + }); + }, 1); + }); + }); + + it('should allow cancelation of fns while the task is being executed', function(done) { + const spy = jasmine.createSpy('spy'); + const cancelId = setTimeout(() => { + clearTimeout(cancelId); + done(); + }, 0); + }); + + it('should allow cancelation of fns registered with setTimeout during invocation', + function(done) { + const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'}); + testZone.run(() => { + const cancelId = setTimeout(function() { + clearTimeout(cancelId); + done(); + }, 0); + }); + }); + + it('should return the original timeout Id', function() { + // Node returns complex object from setTimeout, ignore this test. + if (isNode) return; + const cancelId = setTimeout(() => {}, 0); + expect(typeof cancelId).toEqual('number'); + }); + + it('should allow cancelation by numeric timeout Id', function(done) { + // Node returns complex object from setTimeout, ignore this test. + if (isNode) { + done(); + return; + } + + const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'}); + testZone.run(() => { + const spy = jasmine.createSpy('spy'); + const cancelId = setTimeout(spy, 0); + clearTimeout(cancelId); + setTimeout(function() { + expect(spy).not.toHaveBeenCalled(); + done(); + }, 1); + }); + }); + + it('should pass invalid values through', function() { + clearTimeout(null as any); + clearTimeout({}); + }); +}); diff --git a/packages/zone.js/test/common/task.spec.ts b/packages/zone.js/test/common/task.spec.ts new file mode 100644 index 0000000000..a37365cfbc --- /dev/null +++ b/packages/zone.js/test/common/task.spec.ts @@ -0,0 +1,965 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +const noop = function() {}; +let log: {zone: string, taskZone: undefined | string, toState: TaskState, fromState: TaskState}[] = + []; +const detectTask = Zone.current.scheduleMacroTask('detectTask', noop, undefined, noop, noop); +const originalTransitionTo = detectTask.constructor.prototype._transitionTo; +// patch _transitionTo of ZoneTask to add log for test +const logTransitionTo: Function = function( + toState: TaskState, fromState1: TaskState, fromState2?: TaskState) { + log.push({ + zone: Zone.current.name, + taskZone: this.zone && this.zone.name, + toState: toState, + fromState: this._state + }); + originalTransitionTo.apply(this, arguments); +}; + +function testFnWithLoggedTransitionTo(testFn: Function) { + return function() { + detectTask.constructor.prototype._transitionTo = logTransitionTo; + testFn.apply(this, arguments); + detectTask.constructor.prototype._transitionTo = originalTransitionTo; + }; +} + +describe('task lifecycle', () => { + describe('event task lifecycle', () => { + beforeEach(() => { log = []; }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testEventTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduled to running when task is invoked then from running to scheduled after invoke', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = + Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + })); + + it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = + Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop); + Zone.current.cancelTask(task); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + + it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask( + 'testEventTask', () => { Zone.current.cancelTask(task); }, undefined, noop, noop); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'canceling', fromState: 'running'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + + it('task should transit from running to scheduled when task.callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask( + 'testEventTask', () => { throw Error('invoke error'); }, undefined, noop, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + })); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask( + 'testEventTask', noop, undefined, noop, () => { throw Error('cancel task'); }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + })); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask( + 'testEventTask', noop, undefined, noop, () => { throw Error('cancel task'); }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + })); + + it('task should transit from notScheduled to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testEventTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled', + testFnWithLoggedTransitionTo(() => { + let task: Task; + Zone.current + .fork({ + name: 'testEventTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'canceling') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = + Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop); + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + }); + + describe('non periodical macroTask lifecycle', () => { + beforeEach(() => { log = []; }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testMacroTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduled to running when task is invoked then from running to noScheduled after invoke', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = + Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + })); + + it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = + Zone.current.scheduleMacroTask('testMacrotask', noop, undefined, noop, noop); + Zone.current.cancelTask(task); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + + it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask( + 'testMacroTask', () => { Zone.current.cancelTask(task); }, undefined, noop, noop); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'canceling', fromState: 'running'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + + it('task should transit from running to noScheduled when task.callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask( + 'testMacroTask', () => { throw Error('invoke error'); }, undefined, noop, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + })); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask( + 'testMacroTask', noop, undefined, noop, () => { throw Error('cancel task'); }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + })); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask( + 'testMacroTask', noop, undefined, noop, () => { throw Error('cancel task'); }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + })); + + it('task should transit from notScheduled to scheduling then to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testMacroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error after task.callback being invoked', + testFnWithLoggedTransitionTo(() => { + let task: Task; + Zone.current + .fork({ + name: 'testMacroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'running') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = + Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop); + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + })); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled before running', + testFnWithLoggedTransitionTo(() => { + let task: Task; + Zone.current + .fork({ + name: 'testMacroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'canceling') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = + Zone.current.scheduleMacroTask('testMacroTask', noop, undefined, noop, noop); + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + }); + + describe('periodical macroTask lifecycle', () => { + let task: Task|null; + beforeEach(() => { + log = []; + task = null; + }); + afterEach(() => { + task && task.state !== 'notScheduled' && task.state !== 'canceling' && + task.state !== 'unknown' && task.zone.cancelTask(task); + }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testPeriodicalTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduled to running when task is invoked then from running to scheduled after invoke', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + })); + + it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + Zone.current.cancelTask(task); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + + it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask('testPeriodicalTask', () => { + Zone.current.cancelTask(task !); + }, {isPeriodic: true}, noop, noop); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'canceling', fromState: 'running'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + + it('task should transit from running to scheduled when task.callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask('testPeriodicalTask', () => { + throw Error('invoke error'); + }, {isPeriodic: true}, noop, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + })); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, + () => { throw Error('cancel task'); }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + })); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, + () => { throw Error('cancel task'); }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + })); + + it('task should transit from notScheduled to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testPeriodicalTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testPeriodicalTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'canceling') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + }); + + describe('microTask lifecycle', () => { + beforeEach(() => { log = []; }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testMicroTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + })); + + it('task should transit from scheduled to running when task is invoked then from running to noScheduled after invoke', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + const task = Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + })); + + it('should throw error when try to cancel a microTask', testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + const task = Zone.current.scheduleMicroTask('testMicroTask', () => {}, undefined, noop); + expect(() => { Zone.current.cancelTask(task); }).toThrowError('Task is not cancelable'); + }); + })); + + it('task should transit from running to notScheduled when task.callback throw error', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + const task = Zone.current.scheduleMicroTask( + 'testMicroTask', () => { throw Error('invoke error'); }, undefined, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + })); + + it('task should transit from notScheduled to scheduling then to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + testFnWithLoggedTransitionTo(() => { + Zone.current + .fork({ + name: 'testMicroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + })); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error after task.callback being invoked', + testFnWithLoggedTransitionTo(() => { + let task: Task; + Zone.current + .fork({ + name: 'testMicroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'running') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = Zone.current.scheduleMicroTask('testMicroTask', noop, undefined, noop); + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + })); + + it('task should not run if task transite to notScheduled state which was canceled', + testFnWithLoggedTransitionTo(() => { + let task: Task; + Zone.current.fork({name: 'testCancelZone'}).run(() => { + const task = + Zone.current.scheduleEventTask('testEventTask', noop, undefined, noop, noop); + Zone.current.cancelTask(task); + task.invoke(); + }); + expect(log.map(item => { return {toState: item.toState, fromState: item.fromState}; })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + })); + }); + + describe('reschedule zone', () => { + let callbackLogs: ({pos: string, method: string, zone: string, task: string} | HasTaskState)[]; + const newZone = Zone.root.fork({ + name: 'new', + onScheduleTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + return delegate.scheduleTask(targetZone, task); + }, + onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => { + callbackLogs.push( + {pos: 'before', method: 'onInvokeTask', zone: currZone.name, task: task.zone.name}); + return delegate.invokeTask(targetZone, task, applyThis, applyArgs); + }, + onCancelTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onCancelTask', zone: currZone.name, task: task.zone.name}); + return delegate.cancelTask(targetZone, task); + }, + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + (hasTaskState as any)['zone'] = targetZone.name; + callbackLogs.push(hasTaskState); + return delegate.hasTask(targetZone, hasTaskState); + } + }); + const zone = Zone.root.fork({ + name: 'original', + onScheduleTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + task.cancelScheduleRequest(); + task = newZone.scheduleTask(task); + callbackLogs.push( + {pos: 'after', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + return task; + }, + onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => { + callbackLogs.push( + {pos: 'before', method: 'onInvokeTask', zone: currZone.name, task: task.zone.name}); + return delegate.invokeTask(targetZone, task, applyThis, applyArgs); + }, + onCancelTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onCancelTask', zone: currZone.name, task: task.zone.name}); + return delegate.cancelTask(targetZone, task); + }, + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + (hasTaskState)['zone'] = targetZone.name; + callbackLogs.push(hasTaskState); + return delegate.hasTask(targetZone, hasTaskState); + } + }); + + beforeEach(() => { callbackLogs = []; }); + + it('should be able to reschedule zone when in scheduling state, after that, task will completely go to new zone, has nothing to do with original one', + testFnWithLoggedTransitionTo(() => { + zone.run(() => { + const t = Zone.current.scheduleMacroTask( + 'testRescheduleZoneTask', noop, undefined, noop, noop); + t.invoke(); + }); + + expect(callbackLogs).toEqual([ + {pos: 'before', method: 'onScheduleTask', zone: 'original', task: 'original'}, + {pos: 'before', method: 'onScheduleTask', zone: 'new', task: 'new'}, + {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'new'}, + {pos: 'after', method: 'onScheduleTask', zone: 'original', task: 'new'}, + {pos: 'before', method: 'onInvokeTask', zone: 'new', task: 'new'}, { + microTask: false, + macroTask: false, + eventTask: false, + change: 'macroTask', + zone: 'new' + } + ]); + })); + + it('should not be able to reschedule task in notScheduled / running / canceling state', + testFnWithLoggedTransitionTo(() => { + Zone.current.fork({name: 'rescheduleNotScheduled'}).run(() => { + const t = Zone.current.scheduleMacroTask( + 'testRescheduleZoneTask', noop, undefined, noop, noop); + Zone.current.cancelTask(t); + expect(() => { t.cancelScheduleRequest(); }) + .toThrow(Error( + `macroTask 'testRescheduleZoneTask': can not transition to ` + + `'notScheduled', expecting state 'scheduling', was 'notScheduled'.`)); + }); + + Zone.current + .fork({ + name: 'rescheduleRunning', + onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => { + expect(() => { task.cancelScheduleRequest(); }) + .toThrow(Error( + `macroTask 'testRescheduleZoneTask': can not transition to ` + + `'notScheduled', expecting state 'scheduling', was 'running'.`)); + } + }) + .run(() => { + const t = Zone.current.scheduleMacroTask( + 'testRescheduleZoneTask', noop, undefined, noop, noop); + t.invoke(); + }); + + Zone.current + .fork({ + name: 'rescheduleCanceling', + onCancelTask: (delegate, currZone, targetZone, task) => { + expect(() => { task.cancelScheduleRequest(); }) + .toThrow(Error( + `macroTask 'testRescheduleZoneTask': can not transition to ` + + `'notScheduled', expecting state 'scheduling', was 'canceling'.`)); + } + }) + .run(() => { + const t = Zone.current.scheduleMacroTask( + 'testRescheduleZoneTask', noop, undefined, noop, noop); + Zone.current.cancelTask(t); + }); + })); + + it('can not reschedule a task to a zone which is the descendants of the original zone', + testFnWithLoggedTransitionTo(() => { + const originalZone = Zone.root.fork({ + name: 'originalZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push({ + pos: 'before', + method: 'onScheduleTask', + zone: currZone.name, + task: task.zone.name + }); + task.cancelScheduleRequest(); + task = rescheduleZone.scheduleTask(task); + callbackLogs.push({ + pos: 'after', + method: 'onScheduleTask', + zone: currZone.name, + task: task.zone.name + }); + return task; + } + }); + const rescheduleZone = originalZone.fork({name: 'rescheduleZone'}); + expect(() => { + originalZone.run(() => { + Zone.current.scheduleMacroTask('testRescheduleZoneTask', noop, undefined, noop, noop); + }); + }) + .toThrowError( + 'can not reschedule task to rescheduleZone which is descendants of the original zone originalZone'); + })); + }); +}); diff --git a/packages/zone.js/test/common/toString.spec.ts b/packages/zone.js/test/common/toString.spec.ts new file mode 100644 index 0000000000..839ecbbdf9 --- /dev/null +++ b/packages/zone.js/test/common/toString.spec.ts @@ -0,0 +1,88 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {zoneSymbol} from '../../lib/common/utils'; +import {ifEnvSupports} from '../test-util'; + +const g: any = + typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global; +describe('global function patch', () => { + describe('isOriginal', () => { + it('setTimeout toString should be the same with non patched setTimeout', () => { + expect(Function.prototype.toString.call(setTimeout)) + .toEqual(Function.prototype.toString.call(g[zoneSymbol('setTimeout')])); + }); + + it('MutationObserver toString should be the same with native version', + ifEnvSupports('MutationObserver', () => { + const nativeMutationObserver = g[zoneSymbol('MutationObserver')]; + if (typeof nativeMutationObserver === 'function') { + expect(Function.prototype.toString.call(g['MutationObserver'])) + .toEqual(Function.prototype.toString.call(nativeMutationObserver)); + } else { + expect(Function.prototype.toString.call(g['MutationObserver'])) + .toEqual(Object.prototype.toString.call(nativeMutationObserver)); + } + })); + }); + + describe('isNative', () => { + it('ZoneAwareError toString should look like native', + () => { expect(Function.prototype.toString.call(Error)).toContain('[native code]'); }); + + it('Function toString should look like native', () => { + expect(Function.prototype.toString.call(Function.prototype.toString)) + .toContain('[native code]'); + }); + + it('EventTarget addEventListener should look like native', ifEnvSupports('HTMLElement', () => { + expect(Function.prototype.toString.call(HTMLElement.prototype.addEventListener)) + .toContain('[native code]'); + })); + }); +}); + +describe('ZoneTask', () => { + it('should return handleId.toString if handleId is available', () => { + let macroTask1: any = undefined; + let macroTask2: any = undefined; + let microTask: any = undefined; + const zone = Zone.current.fork({ + name: 'timer', + onScheduleTask: (delegate: ZoneDelegate, curr: Zone, target: Zone, task: Task) => { + if (task.type === 'macroTask') { + if (!macroTask1) { + macroTask1 = task; + } else { + macroTask2 = task; + } + } else if (task.type === 'microTask') { + microTask = task; + } + return task; + } + }); + zone.run(() => { + const id1 = setTimeout(() => {}); + clearTimeout(id1); + const id2 = setTimeout(() => {}); + clearTimeout(id2); + Promise.resolve().then(() => {}); + const macroTask1Str = macroTask1.toString(); + const macroTask2Str = macroTask2.toString(); + expect(typeof macroTask1Str).toEqual('string'); + expect(macroTask1Str).toEqual(id1.toString()); + expect(typeof macroTask2Str).toEqual('string'); + expect(macroTask2Str).toEqual(id2.toString()); + if (macroTask1.data && typeof macroTask1.data.handleId === 'number') { + expect(macroTask1Str).not.toEqual(macroTask2Str); + } + expect(typeof microTask.toString()).toEqual('string'); + }); + }); +}); diff --git a/packages/zone.js/test/common/util.spec.ts b/packages/zone.js/test/common/util.spec.ts new file mode 100644 index 0000000000..0134bfda10 --- /dev/null +++ b/packages/zone.js/test/common/util.spec.ts @@ -0,0 +1,271 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {patchMethod, patchProperty, patchPrototype, zoneSymbol} from '../../lib/common/utils'; + +describe('utils', function() { + describe('patchMethod', () => { + it('should patch target where the method is defined', () => { + let args: any[]|undefined; + let self: any; + class Type { + method(..._args: any[]) { + args = _args; + self = this; + return 'OK'; + } + } + const method = Type.prototype.method; + let delegateMethod: Function; + let delegateSymbol: string; + + const instance = new Type(); + expect(patchMethod(instance, 'method', (delegate: Function, symbol: string, name: string) => { + expect(name).toEqual('method'); + delegateMethod = delegate; + delegateSymbol = symbol; + return function(self, args) { return delegate.apply(self, ['patch', args[0]]); }; + })).toBe(delegateMethod !); + + expect(instance.method('a0')).toEqual('OK'); + expect(args).toEqual(['patch', 'a0']); + expect(self).toBe(instance); + expect(delegateMethod !).toBe(method); + expect(delegateSymbol !).toEqual(zoneSymbol('method')); + expect((Type.prototype as any)[delegateSymbol !]).toBe(method); + }); + + it('should not double patch', () => { + const Type = function() {}; + const method = Type.prototype.method = function() {}; + patchMethod(Type.prototype, 'method', (delegate) => { + return function(self, args: any[]) { return delegate.apply(self, ['patch', ...args]); }; + }); + const pMethod = Type.prototype.method; + expect(pMethod).not.toBe(method); + patchMethod(Type.prototype, 'method', (delegate) => { + return function(self, args) { return delegate.apply(self, ['patch', ...args]); }; + }); + expect(pMethod).toBe(Type.prototype.method); + }); + + it('should not patch property which is not configurable', () => { + const TestType = function() {}; + const originalDefineProperty = (Object as any)[zoneSymbol('defineProperty')]; + if (originalDefineProperty) { + originalDefineProperty( + TestType.prototype, 'nonConfigurableProperty', + {configurable: false, writable: true, value: 'test'}); + } else { + Object.defineProperty( + TestType.prototype, 'nonConfigurableProperty', + {configurable: false, writable: true, value: 'test'}); + } + patchProperty(TestType.prototype, 'nonConfigurableProperty'); + const desc = Object.getOwnPropertyDescriptor(TestType.prototype, 'nonConfigurableProperty'); + expect(desc !.writable).toBeTruthy(); + expect(!desc !.get).toBeTruthy(); + }); + }); + + describe('patchPrototype', () => { + it('non configurable property desc should be patched', () => { + 'use strict'; + const TestFunction: any = function() {}; + const log: string[] = []; + Object.defineProperties(TestFunction.prototype, { + 'property1': { + value: function Property1(callback: Function) { Zone.root.run(callback); }, + writable: true, + configurable: true, + enumerable: true + }, + 'property2': { + value: function Property2(callback: Function) { Zone.root.run(callback); }, + writable: true, + configurable: false, + enumerable: true + } + }); + + const zone = Zone.current.fork({name: 'patch'}); + + zone.run(() => { + const instance = new TestFunction(); + instance.property1(() => { log.push('property1' + Zone.current.name); }); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property1', 'property2']); + log.length = 0; + + patchPrototype(TestFunction.prototype, ['property1', 'property2']); + + zone.run(() => { + const instance = new TestFunction(); + instance.property1(() => { log.push('property1' + Zone.current.name); }); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property1patch', 'property2patch']); + }); + + it('non writable property desc should not be patched', () => { + 'use strict'; + const TestFunction: any = function() {}; + const log: string[] = []; + Object.defineProperties(TestFunction.prototype, { + 'property1': { + value: function Property1(callback: Function) { Zone.root.run(callback); }, + writable: true, + configurable: true, + enumerable: true + }, + 'property2': { + value: function Property2(callback: Function) { Zone.root.run(callback); }, + writable: false, + configurable: true, + enumerable: true + } + }); + + const zone = Zone.current.fork({name: 'patch'}); + + zone.run(() => { + const instance = new TestFunction(); + instance.property1(() => { log.push('property1' + Zone.current.name); }); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property1', 'property2']); + log.length = 0; + + patchPrototype(TestFunction.prototype, ['property1', 'property2']); + + zone.run(() => { + const instance = new TestFunction(); + instance.property1(() => { log.push('property1' + Zone.current.name); }); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property1patch', 'property2']); + }); + + it('readonly property desc should not be patched', () => { + 'use strict'; + const TestFunction: any = function() {}; + const log: string[] = []; + Object.defineProperties(TestFunction.prototype, { + 'property1': { + get: function() { + if (!this._property1) { + this._property1 = function Property2(callback: Function) { Zone.root.run(callback); }; + } + return this._property1; + }, + set: function(func: Function) { this._property1 = func; }, + configurable: true, + enumerable: true + }, + 'property2': { + get: function() { + return function Property2(callback: Function) { Zone.root.run(callback); }; + }, + configurable: true, + enumerable: true + } + }); + + const zone = Zone.current.fork({name: 'patch'}); + + zone.run(() => { + const instance = new TestFunction(); + instance.property1(() => { log.push('property1' + Zone.current.name); }); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property1', 'property2']); + log.length = 0; + + patchPrototype(TestFunction.prototype, ['property1', 'property2']); + + zone.run(() => { + const instance = new TestFunction(); + instance.property1(() => { log.push('property1' + Zone.current.name); }); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property1patch', 'property2']); + }); + + it('non writable method should not be patched', () => { + 'use strict'; + const TestFunction: any = function() {}; + const log: string[] = []; + Object.defineProperties(TestFunction.prototype, { + 'property2': { + value: function Property2(callback: Function) { Zone.root.run(callback); }, + writable: false, + configurable: true, + enumerable: true + } + }); + + const zone = Zone.current.fork({name: 'patch'}); + + zone.run(() => { + const instance = new TestFunction(); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property2']); + log.length = 0; + + patchMethod( + TestFunction.prototype, 'property2', + function(delegate: Function, delegateName: string, name: string) { + return function(self: any, args: any) { log.push('patched property2'); }; + }); + + zone.run(() => { + const instance = new TestFunction(); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property2']); + }); + + it('readonly method should not be patched', () => { + 'use strict'; + const TestFunction: any = function() {}; + const log: string[] = []; + Object.defineProperties(TestFunction.prototype, { + 'property2': { + get: function() { + return function Property2(callback: Function) { Zone.root.run(callback); }; + }, + configurable: true, + enumerable: true + } + }); + + const zone = Zone.current.fork({name: 'patch'}); + + zone.run(() => { + const instance = new TestFunction(); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property2']); + log.length = 0; + + patchMethod( + TestFunction.prototype, 'property2', + function(delegate: Function, delegateName: string, name: string) { + return function(self: any, args: any) { log.push('patched property2'); }; + }); + + zone.run(() => { + const instance = new TestFunction(); + instance.property2(() => { log.push('property2' + Zone.current.name); }); + }); + expect(log).toEqual(['property2']); + }); + }); +}); diff --git a/packages/zone.js/test/common/zone.spec.ts b/packages/zone.js/test/common/zone.spec.ts new file mode 100644 index 0000000000..2f21e1fcc1 --- /dev/null +++ b/packages/zone.js/test/common/zone.spec.ts @@ -0,0 +1,388 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {zoneSymbol} from '../../lib/common/utils'; + +describe('Zone', function() { + const rootZone = Zone.current; + + it('should have a name', function() { expect(Zone.current.name).toBeDefined(); }); + + describe('hooks', function() { + it('should throw if onError is not defined', + function() { expect(function() { Zone.current.run(throwError); }).toThrow(); }); + + + it('should fire onError if a function run by a zone throws', function() { + const errorSpy = jasmine.createSpy('error'); + const myZone = Zone.current.fork({name: 'spy', onHandleError: errorSpy}); + + expect(errorSpy).not.toHaveBeenCalled(); + + expect(function() { myZone.runGuarded(throwError); }).not.toThrow(); + + expect(errorSpy).toHaveBeenCalled(); + }); + + it('should send correct currentZone in hook method when in nested zone', function() { + const zone = Zone.current; + const zoneA = zone.fork({ + name: 'A', + onInvoke: function( + parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) { + expect(currentZone.name).toEqual('A'); + return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source); + } + }); + const zoneB = zoneA.fork({ + name: 'B', + onInvoke: function( + parentDelegate, currentZone, targetZone, callback, applyThis, applyArgs, source) { + expect(currentZone.name).toEqual('B'); + return parentDelegate.invoke(targetZone, callback, applyThis, applyArgs, source); + } + }); + const zoneC = zoneB.fork({name: 'C'}); + zoneC.run(function() {}); + }); + }); + + it('should allow zones to be run from within another zone', function() { + const zone = Zone.current; + const zoneA = zone.fork({name: 'A'}); + const zoneB = zone.fork({name: 'B'}); + + zoneA.run(function() { + zoneB.run(function() { expect(Zone.current).toBe(zoneB); }); + expect(Zone.current).toBe(zoneA); + }); + expect(Zone.current).toBe(zone); + }); + + + describe('wrap', function() { + it('should throw if argument is not a function', function() { + expect(function() { + (Zone.current.wrap)(11); + }).toThrowError('Expecting function got: 11'); + }); + }); + + describe('run out side of current zone', function() { + it('should be able to get root zone', function() { + Zone.current.fork({name: 'testZone'}).run(function() { + expect(Zone.root.name).toEqual(''); + }); + }); + + it('should be able to get run under rootZone', function() { + Zone.current.fork({name: 'testZone'}).run(function() { + Zone.root.run(() => { expect(Zone.current.name).toEqual(''); }); + }); + }); + + it('should be able to get run outside of current zone', function() { + Zone.current.fork({name: 'testZone'}).run(function() { + Zone.root.fork({name: 'newTestZone'}).run(() => { + expect(Zone.current.name).toEqual('newTestZone'); + expect(Zone.current.parent !.name).toEqual(''); + }); + }); + }); + }); + + describe('get', function() { + it('should store properties', function() { + const testZone = Zone.current.fork({name: 'A', properties: {key: 'value'}}); + expect(testZone.get('key')).toEqual('value'); + expect(testZone.getZoneWith('key')).toEqual(testZone); + const childZone = testZone.fork({name: 'B', properties: {key: 'override'}}); + expect(testZone.get('key')).toEqual('value'); + expect(testZone.getZoneWith('key')).toEqual(testZone); + expect(childZone.get('key')).toEqual('override'); + expect(childZone.getZoneWith('key')).toEqual(childZone); + }); + }); + + describe('task', () => { + function noop() {} + let log: any[]; + const zone: Zone = Zone.current.fork({ + name: 'parent', + onHasTask: (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState): + void => { + (hasTaskState as any)['zone'] = target.name; + log.push(hasTaskState); + }, + onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task) => { + // Do nothing to prevent tasks from being run on VM turn; + // Tests run task explicitly. + return task; + } + }); + + beforeEach(() => { log = []; }); + + it('task can only run in the zone of creation', () => { + const task = + zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop); + expect(() => { Zone.current.fork({name: 'anotherZone'}).runTask(task); }) + .toThrowError( + 'A task can only be run in the zone of creation! (Creation: createZone; Execution: anotherZone)'); + task.zone.cancelTask(task); + }); + + it('task can only cancel in the zone of creation', () => { + const task = + zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, undefined, noop, noop); + expect(() => { Zone.current.fork({name: 'anotherZone'}).cancelTask(task); }) + .toThrowError( + 'A task can only be cancelled in the zone of creation! (Creation: createZone; Execution: anotherZone)'); + task.zone.cancelTask(task); + }); + + it('should prevent double cancellation', () => { + const task = + zone.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop); + zone.cancelTask(task); + try { + zone.cancelTask(task); + } catch (e) { + expect(e.message).toContain( + 'macroTask \'test\': can not transition to \'canceling\', expecting state \'scheduled\' or \'running\', was \'notScheduled\'.'); + } + }); + + it('should not decrement counters on periodic tasks', () => { + zone.run(() => { + const task = zone.scheduleMacroTask( + 'test', () => log.push('macroTask'), {isPeriodic: true}, noop, noop); + zone.runTask(task); + zone.runTask(task); + zone.cancelTask(task); + }); + expect(log).toEqual([ + {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'}, + 'macroTask', 'macroTask', { + microTask: false, + macroTask: false, + eventTask: false, + change: 'macroTask', + zone: 'parent' + } + ]); + }); + + it('should notify of queue status change', () => { + zone.run(() => { + const z = Zone.current; + z.runTask(z.scheduleMicroTask('test', () => log.push('microTask'))); + z.cancelTask( + z.scheduleMacroTask('test', () => log.push('macroTask'), undefined, noop, noop)); + z.cancelTask( + z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop)); + }); + expect(log).toEqual([ + {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'}, + 'microTask', + {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'}, + {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'parent'}, + {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'parent'}, + {microTask: false, macroTask: false, eventTask: true, change: 'eventTask', zone: 'parent'}, + { + microTask: false, + macroTask: false, + eventTask: false, + change: 'eventTask', + zone: 'parent' + } + ]); + }); + + it('should notify of queue status change on parent task', () => { + zone.fork({name: 'child'}).run(() => { + const z = Zone.current; + z.runTask(z.scheduleMicroTask('test', () => log.push('microTask'))); + }); + expect(log).toEqual([ + {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'}, + {microTask: true, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'}, + 'microTask', + {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'child'}, + {microTask: false, macroTask: false, eventTask: false, change: 'microTask', zone: 'parent'}, + ]); + }); + + it('should allow rescheduling a task on a separate zone', () => { + const log: any[] = []; + const zone = Zone.current.fork({ + name: 'test-root', + onHasTask: + (delegate: ZoneDelegate, current: Zone, target: Zone, hasTaskState: HasTaskState) => { + (hasTaskState as any)['zone'] = target.name; + log.push(hasTaskState); + } + }); + const left = zone.fork({name: 'left'}); + const right = zone.fork({ + name: 'right', + onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => { + log.push( + {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name}); + // Cancel the current scheduling of the task + task.cancelScheduleRequest(); + // reschedule on a different zone. + task = left.scheduleTask(task); + log.push( + {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name}); + return task; + } + }); + const rchild = right.fork({ + name: 'rchild', + onScheduleTask: (delegate: ZoneDelegate, current: Zone, target: Zone, task: Task): Task => { + log.push( + {pos: 'before', method: 'onScheduleTask', zone: current.name, task: task.zone.name}); + task = delegate.scheduleTask(target, task); + log.push( + {pos: 'after', method: 'onScheduleTask', zone: current.name, task: task.zone.name}); + expect((task as any)._zoneDelegates.map((zd: ZoneDelegate) => zd.zone.name)).toEqual([ + 'left', 'test-root', 'ProxyZone' + ]); + return task; + } + }); + + const task = rchild.scheduleMacroTask('testTask', () => log.push('WORK'), {}, noop, noop); + expect(task.zone).toEqual(left); + log.push(task.zone.name); + task.invoke(); + expect(log).toEqual([ + {pos: 'before', method: 'onScheduleTask', zone: 'rchild', task: 'rchild'}, + {pos: 'before', method: 'onScheduleTask', zone: 'right', task: 'rchild'}, + {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'left'}, { + microTask: false, + macroTask: true, + eventTask: false, + change: 'macroTask', + zone: 'test-root' + }, + {pos: 'after', method: 'onScheduleTask', zone: 'right', task: 'left'}, + {pos: 'after', method: 'onScheduleTask', zone: 'rchild', task: 'left'}, 'left', 'WORK', + {microTask: false, macroTask: false, eventTask: false, change: 'macroTask', zone: 'left'}, { + microTask: false, + macroTask: false, + eventTask: false, + change: 'macroTask', + zone: 'test-root' + } + ]); + }); + + it('period task should not transit to scheduled state after being cancelled in running state', + () => { + const zone = Zone.current.fork({name: 'testZone'}); + + const task = zone.scheduleMacroTask('testPeriodTask', () => { + zone.cancelTask(task); + }, {isPeriodic: true}, () => {}, () => {}); + + task.invoke(); + expect(task.state).toBe('notScheduled'); + }); + + it('event task should not transit to scheduled state after being cancelled in running state', + () => { + const zone = Zone.current.fork({name: 'testZone'}); + + const task = zone.scheduleEventTask( + 'testEventTask', () => { zone.cancelTask(task); }, undefined, () => {}, () => {}); + + task.invoke(); + expect(task.state).toBe('notScheduled'); + }); + + describe('assert ZoneAwarePromise', () => { + it('should not throw when all is OK', () => { Zone.assertZonePatched(); }); + + it('should keep ZoneAwarePromise has been patched', () => { + class WrongPromise { + static resolve(value: any) {} + + then() {} + } + + const ZoneAwarePromise = global.Promise; + const NativePromise = (global as any)[zoneSymbol('Promise')]; + global.Promise = WrongPromise; + try { + expect(ZoneAwarePromise).toBeTruthy(); + Zone.assertZonePatched(); + expect(global.Promise).toBe(ZoneAwarePromise); + } finally { + // restore it. + global.Promise = NativePromise; + } + Zone.assertZonePatched(); + }); + }); + }); + + describe('invoking tasks', () => { + let log: string[]; + function noop() {} + + + beforeEach(() => { log = []; }); + + it('should not drain the microtask queue too early', () => { + const z = Zone.current; + const event = z.scheduleEventTask('test', () => log.push('eventTask'), undefined, noop, noop); + + z.scheduleMicroTask('test', () => log.push('microTask')); + + const macro = z.scheduleMacroTask('test', () => { + event.invoke(); + // At this point, we should not have invoked the microtask. + expect(log).toEqual(['eventTask']); + }, undefined, noop, noop); + + macro.invoke(); + }); + + it('should convert task to json without cyclic error', () => { + const z = Zone.current; + const event = z.scheduleEventTask('test', () => {}, undefined, noop, noop); + const micro = z.scheduleMicroTask('test', () => {}); + const macro = z.scheduleMacroTask('test', () => {}, undefined, noop, noop); + expect(function() { JSON.stringify(event); }).not.toThrow(); + expect(function() { JSON.stringify(micro); }).not.toThrow(); + expect(function() { JSON.stringify(macro); }).not.toThrow(); + }); + + it('should call onHandleError callback when zoneSpec onHasTask throw error', () => { + const spy = jasmine.createSpy('error'); + const hasTaskZone = Zone.current.fork({ + name: 'hasTask', + onHasTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + hasTasState: HasTaskState) => { throw new Error('onHasTask Error'); }, + onHandleError: + (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, error: Error) => { + spy(error.message); + return delegate.handleError(targetZone, error); + } + }); + + const microTask = hasTaskZone.scheduleMicroTask('test', () => {}, undefined, () => {}); + expect(spy).toHaveBeenCalledWith('onHasTask Error'); + }); + }); +}); + +function throwError() { + throw new Error(); +} diff --git a/packages/zone.js/test/common_tests.ts b/packages/zone.js/test/common_tests.ts new file mode 100644 index 0000000000..426762d561 --- /dev/null +++ b/packages/zone.js/test/common_tests.ts @@ -0,0 +1,27 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import './common/microtasks.spec'; +import './common/zone.spec'; +import './common/task.spec'; +import './common/util.spec'; +import './common/Promise.spec'; +import './common/fetch.spec'; +import './common/Error.spec'; +import './common/setInterval.spec'; +import './common/setTimeout.spec'; +import './common/toString.spec'; +import './zone-spec/long-stack-trace-zone.spec'; +import './zone-spec/async-test.spec'; +import './zone-spec/sync-test.spec'; +import './zone-spec/fake-async-test.spec'; +import './zone-spec/proxy.spec'; +import './zone-spec/task-tracking.spec'; +import './rxjs/rxjs.spec'; + +Error.stackTraceLimit = Number.POSITIVE_INFINITY; diff --git a/packages/zone.js/test/extra/bluebird.spec.ts b/packages/zone.js/test/extra/bluebird.spec.ts new file mode 100644 index 0000000000..1f935e4067 --- /dev/null +++ b/packages/zone.js/test/extra/bluebird.spec.ts @@ -0,0 +1,703 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +// test bluebird promise patch +// this spec will not be integrated with Travis CI, because I don't +// want to add bluebird into devDependencies, you can run this spec +// on your local environment +process.on('unhandledRejection', (reason, p) => { + console.log('Unhandled Rejection at:', p, 'reason:', reason); + // application specific logging, throwing an error, or other logic here +}); + +describe('bluebird promise', () => { + let BluebirdPromise: any; + beforeAll(() => { + BluebirdPromise = require('bluebird'); + // import bluebird patch + require('../../lib/extra/bluebird'); + const patchBluebird = (Zone as any)[(Zone as any).__symbol__('bluebird')]; + patchBluebird(BluebirdPromise); + }); + + let log: string[]; + + const zone = Zone.root.fork({ + name: 'bluebird', + onScheduleTask: (delegate, curr, targetZone, task) => { + log.push('schedule bluebird task ' + task.source); + return delegate.scheduleTask(targetZone, task); + }, + onInvokeTask: (delegate, curr, target, task, applyThis, applyArgs) => { + log.push('invoke bluebird task ' + task.source); + return delegate.invokeTask(target, task, applyThis, applyArgs); + } + }); + + beforeEach(() => { log = []; }); + + it('bluebird promise then method should be in zone and treated as microTask', (done) => { + zone.run(() => { + const p = new BluebirdPromise( + (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 0); }); + p.then(() => { + expect(Zone.current.name).toEqual('bluebird'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + done(); + }); + }); + }); + + it('bluebird promise catch method should be in zone and treated as microTask', (done) => { + zone.run(() => { + const p = new BluebirdPromise( + (resolve: any, reject: any) => { setTimeout(() => { reject('test'); }, 0); }); + p.catch(() => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise spread method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.all([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')]) + .spread((r1: string, r2: string) => { + expect(r1).toEqual('test1'); + expect(r2).toEqual('test2'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise finally method should be in zone', (done) => { + zone.run(() => { + const p = new BluebirdPromise( + (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 0); }); + p.finally(() => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise join method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise + .join( + BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2'), + (r1: string, r2: string) => { + expect(r1).toEqual('test1'); + expect(r2).toEqual('test2'); + expect(Zone.current.name).toEqual('bluebird'); + }) + .then(() => { + expect(Zone.current.name).toEqual('bluebird'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + done(); + }); + }); + }); + + it('bluebird promise try method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.try(() => { throw new Error('promise error'); }).catch((err: Error) => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + expect(err.message).toEqual('promise error'); + done(); + }); + }); + }); + + it('bluebird promise method method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.method(() => { return 'test'; })().then((result: string) => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + expect(result).toEqual('test'); + done(); + }); + }); + }); + + it('bluebird promise resolve method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.resolve('test').then((result: string) => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + expect(result).toEqual('test'); + done(); + }); + }); + }); + + it('bluebird promise reject method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.reject('error').catch((error: any) => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + expect(error).toEqual('error'); + done(); + }); + }); + }); + + it('bluebird promise all method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.all([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')]) + .then((r: string[]) => { + expect(r[0]).toEqual('test1'); + expect(r[1]).toEqual('test2'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise props method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise + .props({test1: BluebirdPromise.resolve('test1'), test2: BluebirdPromise.resolve('test2')}) + .then((r: any) => { + expect(r.test1).toEqual('test1'); + expect(r.test2).toEqual('test2'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise any method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.any([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')]) + .then((r: any) => { + expect(r).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise some method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.some([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')], 1) + .then((r: any) => { + expect(r.length).toBe(1); + expect(r[0]).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise map method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise + .map(['test1', 'test2'], (value: any) => { return BluebirdPromise.resolve(value); }) + .then((r: string[]) => { + expect(r.length).toBe(2); + expect(r[0]).toEqual('test1'); + expect(r[1]).toEqual('test2'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise reduce method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise + .reduce( + [1, 2], + (total: string, value: string) => { return BluebirdPromise.resolve(total + value); }) + .then((r: number) => { + expect(r).toBe(3); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBeTruthy(); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length) + .toBeTruthy(); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise filter method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise + .filter( + [1, 2, 3], + (value: number) => { + return value % 2 === 0 ? BluebirdPromise.resolve(true) : + BluebirdPromise.resolve(false); + }) + .then((r: number[]) => { + expect(r[0]).toBe(2); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise each method should be in zone', (done) => { + zone.run(() => { + const arr = [1, 2, 3]; + BluebirdPromise + .each( + BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)), + (r: number[], idx: number) => { + expect(r[idx] === arr[idx]); + expect(Zone.current.name).toEqual('bluebird'); + }) + .then((r: any) => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBeTruthy(); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length) + .toBeTruthy(); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise mapSeries method should be in zone', (done) => { + zone.run(() => { + const arr = [1, 2, 3]; + BluebirdPromise + .mapSeries( + BluebirdPromise.map(arr, (item: number) => BluebirdPromise.resolve(item)), + (r: number[], idx: number) => { + expect(r[idx] === arr[idx]); + expect(Zone.current.name).toEqual('bluebird'); + }) + .then((r: any) => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBeTruthy(); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length) + .toBeTruthy(); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + ; + }); + }); + + it('bluebird promise race method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.race([BluebirdPromise.resolve('test1'), BluebirdPromise.resolve('test2')]) + .then((r: string) => { + expect(r).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise using/disposer method should be in zone', (done) => { + zone.run(() => { + const p = new BluebirdPromise( + (resolve: Function, reject: any) => { setTimeout(() => { resolve('test'); }, 0); }); + p.leakObj = []; + const disposer = p.disposer(() => { p.leakObj = null; }); + BluebirdPromise.using(disposer, (v: string) => { p.leakObj.push(v); }).then(() => { + expect(Zone.current.name).toEqual('bluebird'); + expect(p.leakObj).toBe(null); + // using will generate several promise inside bluebird + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBeTruthy(); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length) + .toBeTruthy(); + done(); + }); + }); + }); + + it('bluebird promise promisify method should be in zone and treated as microTask', (done) => { + const func = (cb: Function) => { setTimeout(() => { cb(null, 'test'); }, 10); }; + + const promiseFunc = BluebirdPromise.promisify(func); + zone.run(() => { + promiseFunc().then((r: string) => { + expect(Zone.current.name).toEqual('bluebird'); + expect(r).toBe('test'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + done(); + }); + }); + }); + + it('bluebird promise promisifyAll method should be in zone', (done) => { + const obj = { + func1: (cb: Function) => { setTimeout(() => { cb(null, 'test1'); }, 10); }, + func2: (cb: Function) => { setTimeout(() => { cb(null, 'test2'); }, 10); }, + }; + + const promiseObj = BluebirdPromise.promisifyAll(obj); + zone.run(() => { + BluebirdPromise.all([promiseObj.func1Async(), promiseObj.func2Async()]) + .then((r: string[]) => { + expect(Zone.current.name).toEqual('bluebird'); + expect(r[0]).toBe('test1'); + expect(r[1]).toBe('test2'); + // using will generate several promise inside + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + done(); + }); + }); + }); + + it('bluebird promise fromCallback method should be in zone', (done) => { + const resolver = (cb: Function) => { setTimeout(() => { cb(null, 'test'); }, 10); }; + + zone.run(() => { + BluebirdPromise.fromCallback(resolver).then((r: string) => { + expect(Zone.current.name).toEqual('bluebird'); + expect(r).toBe('test'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + done(); + }); + }); + }); + + it('bluebird promise asCallback method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.resolve('test').asCallback((err: Error, r: string) => { + expect(Zone.current.name).toEqual('bluebird'); + expect(r).toBe('test'); + done(); + }); + }); + }); + + it('bluebird promise delay method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.resolve('test').delay(10).then((r: string) => { + expect(Zone.current.name).toEqual('bluebird'); + expect(r).toBe('test'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + done(); + }); + }); + }); + + it('bluebird promise timeout method should be in zone', (done) => { + zone.run(() => { + new BluebirdPromise( + (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 10); }) + .timeout(100) + .then((r: string) => { + expect(Zone.current.name).toEqual('bluebird'); + expect(r).toBe('test'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + done(); + }); + }); + }); + + it('bluebird promise tap method should be in zone', (done) => { + zone.run(() => { + const p = new BluebirdPromise( + (resolve: any, reject: any) => { setTimeout(() => { resolve('test'); }, 0); }); + p.tap(() => { expect(Zone.current.name).toEqual('bluebird'); }).then(() => { + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise call method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise + .map(['test1', 'test2'], (value: any) => { return BluebirdPromise.resolve(value); }) + .call('shift', (value: any) => { return value; }) + .then((r: string) => { + expect(r).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length) + .toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise get method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.resolve(['test1', 'test2']).get(-1).then((r: string) => { + expect(r).toEqual('test2'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise return method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.resolve().return ('test1').then((r: string) => { + expect(r).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise throw method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.resolve().throw('test1').catch((r: string) => { + expect(r).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise catchReturn method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.reject().catchReturn('test1').then((r: string) => { + expect(r).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise catchThrow method should be in zone', (done) => { + zone.run(() => { + BluebirdPromise.reject().catchThrow('test1').catch((r: string) => { + expect(r).toEqual('test1'); + expect(log.filter(item => item === 'schedule bluebird task Promise.then').length).toBe(1); + expect(log.filter(item => item === 'invoke bluebird task Promise.then').length).toBe(1); + expect(Zone.current.name).toEqual('bluebird'); + done(); + }); + }); + }); + + it('bluebird promise reflect method should be in zone', (done) => { + zone.run(() => { + const promises = [BluebirdPromise.resolve('test1'), BluebirdPromise.reject('test2')]; + BluebirdPromise.all(promises.map(promise => { return promise.reflect(); })).each((r: any) => { + if (r.isFulfilled()) { + expect(r.value()).toEqual('test1'); + } else { + expect(r.reason()).toEqual('test2'); + done(); + } + expect(Zone.current.name).toEqual('bluebird'); + }); + }); + }); + + it('bluebird should be able to run into different zone', (done: Function) => { + Zone.current.fork({name: 'zone_A'}).run(() => { + new BluebirdPromise((resolve: any, reject: any) => { + expect(Zone.current.name).toEqual('zone_A'); + resolve(1); + }).then((r: any) => { expect(Zone.current.name).toEqual('zone_A'); }); + }); + + Zone.current.fork({name: 'zone_B'}).run(() => { + new BluebirdPromise((resolve: any, reject: any) => { + expect(Zone.current.name).toEqual('zone_B'); + resolve(2); + }).then((r: any) => { + expect(Zone.current.name).toEqual('zone_B'); + done(); + }); + }); + }); + + it('should be able to chain promise', (done: DoneFn) => { + Zone.current.fork({name: 'zone_A'}).run(() => { + new BluebirdPromise((resolve: any, reject: any) => { + expect(Zone.current.name).toEqual('zone_A'); + resolve(1); + }) + .then((r: any) => { + expect(r).toBe(1); + expect(Zone.current.name).toEqual('zone_A'); + return Promise.resolve(2); + }) + .then((r: any) => { + expect(r).toBe(2); + expect(Zone.current.name).toEqual('zone_A'); + }); + }); + Zone.current.fork({name: 'zone_B'}).run(() => { + new BluebirdPromise((resolve: any, reject: any) => { + expect(Zone.current.name).toEqual('zone_B'); + reject(1); + }) + .then( + () => { fail('should not be here.'); }, + (r: any) => { + expect(r).toBe(1); + expect(Zone.current.name).toEqual('zone_B'); + return Promise.resolve(2); + }) + .then((r: any) => { + expect(r).toBe(2); + expect(Zone.current.name).toEqual('zone_B'); + done(); + }); + }); + }); + + it('should catch rejected chained bluebird promise', (done: DoneFn) => { + const logs: string[] = []; + const zone = Zone.current.fork({ + name: 'testErrorHandling', + onHandleError: function() { + // should not get here + logs.push('onHandleError'); + return true; + } + }); + + zone.runGuarded(() => { + return BluebirdPromise.resolve().then(() => { throw new Error('test error'); }).catch(() => { + expect(logs).toEqual([]); + done(); + }); + }); + }); + + it('should catch rejected chained global promise', (done: DoneFn) => { + const logs: string[] = []; + const zone = Zone.current.fork({ + name: 'testErrorHandling', + onHandleError: function() { + // should not get here + logs.push('onHandleError'); + return true; + } + }); + + zone.runGuarded(() => { + return Promise.resolve().then(() => { throw new Error('test error'); }).catch(() => { + expect(logs).toEqual([]); + done(); + }); + }); + }); + + it('should catch rejected bluebird promise', (done: DoneFn) => { + const logs: string[] = []; + const zone = Zone.current.fork({ + name: 'testErrorHandling', + onHandleError: function() { + // should not get here + logs.push('onHandleError'); + return true; + } + }); + + zone.runGuarded(() => { + return BluebirdPromise.reject().catch(() => { + expect(logs).toEqual([]); + done(); + }); + }); + }); + + it('should catch rejected global promise', (done: DoneFn) => { + const logs: string[] = []; + const zone = Zone.current.fork({ + name: 'testErrorHandling', + onHandleError: function() { + // should not get here + logs.push('onHandleError'); + return true; + } + }); + + zone.runGuarded(() => { + return Promise.reject(new Error('reject')).catch(() => { + expect(logs).toEqual([]); + done(); + }); + }); + }); + + it('should trigger onHandleError when unhandledRejection', (done: DoneFn) => { + const zone = Zone.current.fork({ + name: 'testErrorHandling', + onHandleError: function() { + setTimeout(done, 100); + return true; + } + }); + + zone.runGuarded(() => { return Promise.reject(new Error('reject')); }); + }); + + it('should trigger onHandleError when unhandledRejection in chained Promise', (done: DoneFn) => { + const zone = Zone.current.fork({ + name: 'testErrorHandling', + onHandleError: function() { + setTimeout(done, 100); + return true; + } + }); + + zone.runGuarded(() => { return Promise.resolve().then(() => { throw new Error('test'); }); }); + }); +}); diff --git a/packages/zone.js/test/extra/cordova.spec.ts b/packages/zone.js/test/extra/cordova.spec.ts new file mode 100644 index 0000000000..3339ac4849 --- /dev/null +++ b/packages/zone.js/test/extra/cordova.spec.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +describe('cordova test', () => { + it('cordova.exec() should be patched as macroTask', (done) => { + const cordova = (window as any).cordova; + if (!cordova) { + done(); + return; + } + + const zone = Zone.current.fork({name: 'cordova'}); + + zone.run(() => { + cordova.exec( + () => { + expect(Zone.current.name).toEqual('cordova'); + done(); + }, + () => { fail('should not fail'); }, 'service', 'successAction', ['arg0', 'arg1']); + + cordova.exec( + () => { fail('should not success'); }, + () => { + expect(Zone.current.name).toEqual('cordova'); + done(); + }, + 'service', 'failAction', ['arg0', 'arg1']); + }); + }); +}); \ No newline at end of file diff --git a/packages/zone.js/test/fake_entry.js b/packages/zone.js/test/fake_entry.js new file mode 100644 index 0000000000..ec965799df --- /dev/null +++ b/packages/zone.js/test/fake_entry.js @@ -0,0 +1 @@ +var TEST = 'TEST'; diff --git a/packages/zone.js/test/jasmine-patch.spec.ts b/packages/zone.js/test/jasmine-patch.spec.ts new file mode 100644 index 0000000000..2765aceace --- /dev/null +++ b/packages/zone.js/test/jasmine-patch.spec.ts @@ -0,0 +1,76 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ifEnvSupports} from './test-util'; + +function supportJasmineSpec() { + return jasmine && (jasmine as any)['Spec']; +} + +(supportJasmineSpec as any).message = 'jasmine spec'; + +ifEnvSupports(supportJasmineSpec, () => { + beforeEach(() => { + // assert that each jasmine run has a task, so that drainMicrotask works properly. + expect(Zone.currentTask).toBeTruthy(); + }); + + describe('jasmine', () => { + let throwOnAsync = false; + let beforeEachZone: Zone|null = null; + let beforeAllZone: Zone|null = null; + let itZone: Zone|null = null; + const syncZone = Zone.current; + try { + Zone.current.scheduleMicroTask('dontallow', (): any => null); + } catch (e) { + throwOnAsync = true; + } + + beforeAll(() => beforeAllZone = Zone.current); + + beforeEach(() => beforeEachZone = Zone.current); + + it('should throw on async in describe', () => { + expect(throwOnAsync).toBe(true); + expect(syncZone.name).toEqual('syncTestZone for jasmine.describe'); + itZone = Zone.current; + }); + + it('should cope with pending tests, which have no test body'); + + afterEach(() => { + let zone = Zone.current; + expect(zone.name).toEqual('ProxyZone'); + expect(beforeEachZone !.name).toEqual(zone.name); + expect(itZone).toBe(zone); + }); + + afterAll(() => { + let zone = Zone.current; + expect(zone.name).toEqual('ProxyZone'); + expect(beforeAllZone !.name).toEqual(zone.name); + }); + }); + + describe('return promise', () => { + let log: string[]; + beforeEach(() => { log = []; }); + + it('should wait for promise to resolve', () => { + return new Promise((res, _) => { + setTimeout(() => { + log.push('resolved'); + res(); + }, 100); + }); + }); + + afterEach(() => { expect(log).toEqual(['resolved']); }); + }); +})(); diff --git a/packages/zone.js/test/main.ts b/packages/zone.js/test/main.ts new file mode 100644 index 0000000000..84e1a0fa85 --- /dev/null +++ b/packages/zone.js/test/main.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/// + +declare const __karma__: { + loaded: Function, + start: Function, + error: Function, +}; + +__karma__.loaded = function() {}; + +let entryPoint = 'browser_entry_point'; + +if (typeof __karma__ !== 'undefined') { + (window as any)['__Zone_Error_BlacklistedStackFrames_policy'] = + (__karma__ as any).config.errorpolicy; + if ((__karma__ as any).config.entrypoint) { + entryPoint = (__karma__ as any).config.entrypoint; + } +} else if (typeof process !== 'undefined') { + (window as any)['__Zone_Error_BlacklistedStackFrames_policy'] = process.env.errorpolicy; + if (process.env.entrypoint) { + entryPoint = process.env.entrypoint; + } +} + +(window as any).global = window; +System.config({ + defaultJSExtensions: true, + map: { + 'rxjs': 'base/npm/node_modules/rxjs/index', + 'rxjs/operators': 'base/npm/node_modules/rxjs/operators/index', + 'core-js/features/set': 'base/npm/node_modules/core-js/es6/set', + 'core-js/features/map': 'base/npm/node_modules/core-js/es6/map', + 'es6-promise': 'base/npm/node_modules/es6-promise/dist/es6-promise' + }, +}); + +let browserPatchedPromise: any = null; +if ((window as any)[(Zone as any).__symbol__('setTimeout')]) { + browserPatchedPromise = Promise.resolve('browserPatched'); +} else { + // this means that Zone has not patched the browser yet, which means we must be running in + // build mode and need to load the browser patch. + browserPatchedPromise = + System.import('/base/angular/packages/zone.js/test/browser-zone-setup').then(() => { + let testFrameworkPatch = typeof(window as any).Mocha !== 'undefined' ? + '/base/angular/packages/zone.js/lib/mocha/mocha' : + '/base/angular/packages/zone.js/lib/jasmine/jasmine'; + return System.import(testFrameworkPatch); + }); +} + +browserPatchedPromise.then(() => { + let testFrameworkPatch = typeof(window as any).Mocha !== 'undefined' ? + '/base/angular/packages/zone.js/test/test-env-setup-mocha' : + '/base/angular/packages/zone.js/test/test-env-setup-jasmine'; + // Setup test environment + System.import(testFrameworkPatch).then(() => { + System.import('/base/angular/packages/zone.js/lib/common/error-rewrite').then(() => { + System.import(`/base/angular/packages/zone.js/test/${entryPoint}`) + .then( + () => { __karma__.start(); }, + (error: any) => { console.error(error.stack || error); }); + }); + }); +}); diff --git a/packages/zone.js/test/mocha-patch.spec.ts b/packages/zone.js/test/mocha-patch.spec.ts new file mode 100644 index 0000000000..ef19273cb5 --- /dev/null +++ b/packages/zone.js/test/mocha-patch.spec.ts @@ -0,0 +1,104 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Extra Mocha-specific typings to make sure typescript compiler is happy +// Didn't want to add @types/mocha because of duplication in typings-file with @types/jasmine +declare function suite(description: string, suiteFn: () => void): void; + declare function test(description: string, testFn: () => void): void; + declare function specify(description: string, testFn: () => void): void; + declare function setup(fn: () => void): void; declare function teardown(fn: () => void): void; + declare function suiteSetup(fn: () => void): void; + declare function suiteTeardown(fn: () => void): void; + declare function before(fn: () => void): void; declare function after(fn: () => void): void; + // + + import { + ifEnvSupports + } from './test-util'; + +ifEnvSupports('Mocha', function() { + describe('Mocha BDD-style', () => { + let throwOnAsync = false; + let beforeEachZone: Zone|null = null; + let itZone: Zone|null = null; + const syncZone = Zone.current; + let beforeZone: Zone|null = null; + + before(() => { beforeZone = Zone.current; }); + + try { + Zone.current.scheduleMicroTask('dontallow', (): any => null); + } catch (e) { + throwOnAsync = true; + } + + beforeEach(() => beforeEachZone = Zone.current); + + it('should throw on async in describe', () => { + expect(Zone.currentTask).toBeTruthy(); + expect(throwOnAsync).toBe(true); + expect(syncZone.name).toEqual('syncTestZone for Mocha.describe'); + itZone = Zone.current; + }); + + afterEach(() => { + let zone = Zone.current; + expect(zone.name).toEqual('ProxyZone'); + expect(beforeEachZone).toBe(zone); + expect(itZone).toBe(zone); + }); + + after(() => { expect(beforeZone).toBe(Zone.current); }); + }); + + suite('Mocha TDD-style', () => { + let testZone: Zone|null = null; + let beforeEachZone: Zone|null = null; + let suiteSetupZone: Zone|null = null; + + suiteSetup(() => { suiteSetupZone = Zone.current; }); + + setup(() => { beforeEachZone = Zone.current; }); + + test('should run in Zone with "test"-syntax in TDD-mode', () => { + testZone = Zone.current; + expect(Zone.currentTask).toBeTruthy(); + expect(testZone.name).toEqual('ProxyZone'); + }); + + specify('test should run in Zone with "specify"-syntax in TDD-mode', () => { + testZone = Zone.current; + expect(Zone.currentTask).toBeTruthy(); + expect(testZone.name).toEqual('ProxyZone'); + }); + + teardown(() => { + expect(Zone.current.name).toEqual('ProxyZone'); + expect(beforeEachZone).toBe(Zone.current); + expect(testZone).toBe(Zone.current); + }); + + suiteTeardown(() => { expect(suiteSetupZone).toBe(Zone.current); }); + }); + + describe('return promise', () => { + let log: string[]; + beforeEach(() => { log = []; }); + + it('should wait for promise to resolve', () => { + return new Promise((res, _) => { + setTimeout(() => { + log.push('resolved'); + res(); + }, 100); + }); + }); + + afterEach(() => { expect(log).toEqual(['resolved']); }); + }); +})(); \ No newline at end of file diff --git a/packages/zone.js/test/node-env-setup.ts b/packages/zone.js/test/node-env-setup.ts new file mode 100644 index 0000000000..77f6606835 --- /dev/null +++ b/packages/zone.js/test/node-env-setup.ts @@ -0,0 +1,2 @@ +// Change default symbol prefix for testing to ensure no hard-coded references. +(global as any)['__Zone_symbol_prefix'] = '__zone_symbol_test__'; diff --git a/packages/zone.js/test/node/Error.spec.ts b/packages/zone.js/test/node/Error.spec.ts new file mode 100644 index 0000000000..114b54fb83 --- /dev/null +++ b/packages/zone.js/test/node/Error.spec.ts @@ -0,0 +1,40 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +describe('ZoneAwareError', () => { + // If the environment does not supports stack rewrites, then these tests will fail + // and there is no point in running them. + if (!(Error as any)['stackRewrite']) return; + + it('should have all properties from NativeError', () => { + let obj: any = new Object(); + Error.captureStackTrace(obj); + expect(obj.stack).not.toBeUndefined(); + }); + + it('should support prepareStackTrace', () => { + const originalPrepareStackTrace = (Error).prepareStackTrace; + (Error).prepareStackTrace = function(error: Error, stack: string) { return stack; }; + let obj: any = new Object(); + Error.captureStackTrace(obj); + expect(obj.stack[0].getFileName()).not.toBeUndefined(); + (Error).prepareStackTrace = originalPrepareStackTrace; + }); + + it('should not add additional stacktrace from Zone when use prepareStackTrace', () => { + const originalPrepareStackTrace = (Error).prepareStackTrace; + (Error).prepareStackTrace = function(error: Error, stack: string) { return stack; }; + let obj: any = new Object(); + Error.captureStackTrace(obj); + expect(obj.stack.length).not.toBe(0); + obj.stack.forEach(function(st: any) { + expect(st.getFunctionName()).not.toEqual('zoneCaptureStackTrace'); + }); + (Error).prepareStackTrace = originalPrepareStackTrace; + }); +}); diff --git a/packages/zone.js/test/node/console.spec.ts b/packages/zone.js/test/node/console.spec.ts new file mode 100644 index 0000000000..c41bdb32a4 --- /dev/null +++ b/packages/zone.js/test/node/console.spec.ts @@ -0,0 +1,39 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +describe('node console', () => { + const log: string[] = []; + const zone = Zone.current.fork({ + name: 'console', + onScheduleTask: function( + delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task) { + log.push(task.source); + return delegate.scheduleTask(targetZone, task); + } + }); + + beforeEach(() => { log.length = 0; }); + + it('console methods should run in root zone', () => { + zone.run(() => { + console.log('test'); + console.warn('test'); + console.error('test'); + console.info('test'); + console.trace('test'); + try { + console.assert(false, 'test'); + } catch (error) { + } + console.dir('.'); + console.time('start'); + console.timeEnd('start'); + console.debug && console.debug('test'); + }); + expect(log).toEqual([]); + }); +}); \ No newline at end of file diff --git a/packages/zone.js/test/node/crypto.spec.ts b/packages/zone.js/test/node/crypto.spec.ts new file mode 100644 index 0000000000..086e1a4ea5 --- /dev/null +++ b/packages/zone.js/test/node/crypto.spec.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +describe('crypto test', () => { + let crypto: any = null; + + try { + crypto = require('crypto'); + } catch (err) { + } + + it('crypto randomBytes method should be patched as tasks', (done) => { + if (!crypto) { + done(); + return; + } + const zoneASpec = { + name: 'A', + onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): + Task => { return delegate.scheduleTask(targetZone, task); } + }; + const zoneA = Zone.current.fork(zoneASpec); + spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); + zoneA.run(() => { + crypto.randomBytes(256, (err: Error, buf: any) => { + expect(err).toBeFalsy(); + expect(zoneASpec.onScheduleTask).toHaveBeenCalled(); + expect(buf.length).toBe(256); + expect(Zone.current.name).toEqual('A'); + done(); + }); + }); + }); + + it('crypto pbkdf2 method should be patched as tasks', (done) => { + if (!crypto) { + done(); + return; + } + const zoneASpec: ZoneSpec = { + name: 'A', + onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): + Task => { return delegate.scheduleTask(targetZone, task); } + }; + const zoneA = Zone.current.fork(zoneASpec); + spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); + zoneA.run(() => { + crypto.pbkdf2('secret', 'salt', 100000, 512, 'sha512', (err: Error, key: any) => { + expect(err).toBeFalsy(); + expect(zoneASpec.onScheduleTask).toHaveBeenCalled(); + expect(key.toString('hex')) + .toEqual( + '3745e482c6e0ade35da10139e797157f4a5da669dad7d5da88ef87e47471cc47ed941c7ad618e827304f083f8707f12b7cfdd5f489b782f10cc269e3c08d59ae04919ee902c99dba309cde75569fbe8e6d5c341d6f2576f6618c589e77911a261ee964e242797e64aeca9a134de5ced37fe2521d35d87303edb55a844c8cf11e3b42b18dbd7add0739ea9b172dc3810f911396fa3956f499415db35b79488d74926cdc0c15c3910bf2e4918f5a8efd7de3d4c314bace50c7a95150339eccd32dda2e15d961ea2c91eddd8b03110135a72b3562f189c2d15568854f9a1844cfa62fb77214f2810a2277fd21be95a794cde78e0fe5267a2c1b0894c7729fc4be378156aeb1cff8a215bb4df12312ba676fe2f270dfc3e2b54d8f9c74dfb531530042a09b226fafbcef45368a1ec75f9224a80f2280f75258ff74a2b9a864d857ede49af6a23af837a1f502a6c32e3537402280bef200d847d8fee42649e6d9a00df952ab2fbefc84ba8927f73137fdfbea81f86088edd4cf329edf3f6982429797143cbd43128777c2da269fadd55d18c7921308c7ad7a5bb85ef8d614e2e8461ea3b7fc2edcf72b85da6828a4198c46000953afb1f3a19ecac0df0d660848a0f89ed3d0e0a82115347c9918bdf16fad479c1de16a6b9798437622acff245e6cf80c9ee9d56cada8523ebb6ff348c73c836e5828761f8dda1dd5ab1633caa39b34'); + expect(Zone.current.name).toEqual('A'); + done(); + }); + }); + }); +}); diff --git a/packages/zone.js/test/node/events.spec.ts b/packages/zone.js/test/node/events.spec.ts new file mode 100644 index 0000000000..0fdc77fbf5 --- /dev/null +++ b/packages/zone.js/test/node/events.spec.ts @@ -0,0 +1,188 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {EventEmitter} from 'events'; + +describe('nodejs EventEmitter', () => { + let zone: Zone, zoneA: Zone, zoneB: Zone, emitter: EventEmitter, expectZoneACount: number, + zoneResults: string[]; + beforeEach(() => { + zone = Zone.current; + zoneA = zone.fork({name: 'A'}); + zoneB = zone.fork({name: 'B'}); + + emitter = new EventEmitter(); + expectZoneACount = 0; + + zoneResults = []; + }); + + function expectZoneA(value: string) { + expectZoneACount++; + expect(Zone.current.name).toBe('A'); + expect(value).toBe('test value'); + } + + function listenerA() { zoneResults.push('A'); } + + function listenerB() { zoneResults.push('B'); } + + function shouldNotRun() { fail('this listener should not run'); } + + it('should register listeners in the current zone', () => { + zoneA.run(() => { + emitter.on('test', expectZoneA); + emitter.addListener('test', expectZoneA); + }); + zoneB.run(() => emitter.emit('test', 'test value')); + expect(expectZoneACount).toBe(2); + }); + it('allows chaining methods', () => { + zoneA.run(() => { + expect(emitter.on('test', expectZoneA)).toBe(emitter); + expect(emitter.addListener('test', expectZoneA)).toBe(emitter); + }); + }); + it('should remove listeners properly', () => { + zoneA.run(() => { + emitter.on('test', shouldNotRun); + emitter.on('test2', shouldNotRun); + emitter.removeListener('test', shouldNotRun); + }); + zoneB.run(() => { + emitter.removeListener('test2', shouldNotRun); + emitter.emit('test', 'test value'); + emitter.emit('test2', 'test value'); + }); + }); + it('remove listener should return event emitter', () => { + zoneA.run(() => { + emitter.on('test', shouldNotRun); + expect(emitter.removeListener('test', shouldNotRun)).toEqual(emitter); + emitter.emit('test', 'test value'); + }); + }); + it('should return all listeners for an event', () => { + zoneA.run(() => { emitter.on('test', expectZoneA); }); + zoneB.run(() => { emitter.on('test', shouldNotRun); }); + expect(emitter.listeners('test')).toEqual([expectZoneA, shouldNotRun]); + }); + it('should return empty array when an event has no listeners', + () => { zoneA.run(() => { expect(emitter.listeners('test')).toEqual([]); }); }); + it('should prepend listener by order', () => { + zoneA.run(() => { + emitter.on('test', listenerA); + emitter.on('test', listenerB); + expect(emitter.listeners('test')).toEqual([listenerA, listenerB]); + emitter.emit('test'); + expect(zoneResults).toEqual(['A', 'B']); + zoneResults = []; + + emitter.removeAllListeners('test'); + + emitter.on('test', listenerA); + emitter.prependListener('test', listenerB); + expect(emitter.listeners('test')).toEqual([listenerB, listenerA]); + emitter.emit('test'); + expect(zoneResults).toEqual(['B', 'A']); + }); + }); + it('should remove All listeners properly', () => { + zoneA.run(() => { + emitter.on('test', expectZoneA); + emitter.on('test', expectZoneA); + emitter.removeAllListeners('test'); + expect(emitter.listeners('test').length).toEqual(0); + }); + }); + it('remove All listeners should return event emitter', () => { + zoneA.run(() => { + emitter.on('test', expectZoneA); + emitter.on('test', expectZoneA); + expect(emitter.removeAllListeners('test')).toEqual(emitter); + expect(emitter.listeners('test').length).toEqual(0); + }); + }); + it('should remove All listeners properly even without a type parameter', () => { + zoneA.run(() => { + emitter.on('test', shouldNotRun); + emitter.on('test1', shouldNotRun); + emitter.removeAllListeners(); + expect(emitter.listeners('test').length).toEqual(0); + expect(emitter.listeners('test1').length).toEqual(0); + }); + }); + it('should remove once listener after emit', () => { + zoneA.run(() => { + emitter.once('test', expectZoneA); + emitter.emit('test', 'test value'); + expect(emitter.listeners('test').length).toEqual(0); + }); + }); + it('should remove once listener properly before listener triggered', () => { + zoneA.run(() => { + emitter.once('test', shouldNotRun); + emitter.removeListener('test', shouldNotRun); + emitter.emit('test'); + }); + }); + it('should trigger removeListener when remove listener', () => { + zoneA.run(() => { + emitter.on('removeListener', function(type: string, handler: any) { + zoneResults.push('remove' + type); + }); + emitter.on( + 'newListener', function(type: string, handler: any) { zoneResults.push('new' + type); }); + emitter.on('test', shouldNotRun); + emitter.removeListener('test', shouldNotRun); + expect(zoneResults).toEqual(['newtest', 'removetest']); + }); + }); + it('should trigger removeListener when remove all listeners with eventname ', () => { + zoneA.run(() => { + emitter.on('removeListener', function(type: string, handler: any) { + zoneResults.push('remove' + type); + }); + emitter.on('test', shouldNotRun); + emitter.on('test1', expectZoneA); + emitter.removeAllListeners('test'); + expect(zoneResults).toEqual(['removetest']); + expect(emitter.listeners('removeListener').length).toBe(1); + }); + }); + it('should trigger removeListener when remove all listeners without eventname', () => { + zoneA.run(() => { + emitter.on('removeListener', function(type: string, handler: any) { + zoneResults.push('remove' + type); + }); + emitter.on('test', shouldNotRun); + emitter.on('test1', shouldNotRun); + emitter.removeAllListeners(); + expect(zoneResults).toEqual(['removetest', 'removetest1']); + expect(emitter.listeners('test').length).toBe(0); + expect(emitter.listeners('test1').length).toBe(0); + expect(emitter.listeners('removeListener').length).toBe(0); + }); + }); + it('should not enter endless loop when register uncaughtException to process', () => { + require('domain'); + zoneA.run(() => { process.on('uncaughtException', function() {}); }); + }); + it('should be able to addEventListener with symbol eventName', () => { + zoneA.run(() => { + const testSymbol = Symbol('test'); + const test1Symbol = Symbol('test1'); + emitter.on(testSymbol, expectZoneA); + emitter.on(test1Symbol, shouldNotRun); + emitter.removeListener(test1Symbol, shouldNotRun); + expect(emitter.listeners(testSymbol).length).toBe(1); + expect(emitter.listeners(test1Symbol).length).toBe(0); + emitter.emit(testSymbol, 'test value'); + }); + }); +}); diff --git a/packages/zone.js/test/node/fs.spec.ts b/packages/zone.js/test/node/fs.spec.ts new file mode 100644 index 0000000000..e6f265c627 --- /dev/null +++ b/packages/zone.js/test/node/fs.spec.ts @@ -0,0 +1,145 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {closeSync, exists, fstatSync, openSync, read, unlink, unlinkSync, unwatchFile, watch, watchFile, write, writeFile} from 'fs'; +import * as util from 'util'; + +describe('nodejs file system', () => { + describe('async method patch test', () => { + it('has patched exists()', (done) => { + const zoneA = Zone.current.fork({name: 'A'}); + zoneA.run(() => { + exists('testfile', (_) => { + expect(Zone.current.name).toBe(zoneA.name); + done(); + }); + }); + }); + + it('has patched exists as macroTask', (done) => { + const zoneASpec = { + name: 'A', + onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): + Task => { return delegate.scheduleTask(targetZone, task); } + }; + const zoneA = Zone.current.fork(zoneASpec); + spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); + zoneA.run(() => { + exists('testfile', (_) => { + expect(zoneASpec.onScheduleTask).toHaveBeenCalled(); + done(); + }); + }); + }); + }); + + describe('watcher related methods test', () => { + const zoneASpec = { + name: 'A', + onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): + Task => { return delegate.scheduleTask(targetZone, task); } + }; + + it('fs.watch has been patched as eventTask', (done) => { + spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); + const zoneA = Zone.current.fork(zoneASpec); + zoneA.run(() => { + writeFile('testfile', 'test content', () => { + const watcher = watch('testfile', (eventType, filename) => { + expect(filename).toEqual('testfile'); + expect(eventType).toEqual('change'); + expect(zoneASpec.onScheduleTask).toHaveBeenCalled(); + expect(Zone.current.name).toBe('A'); + watcher.close(); + unlink('testfile', () => { done(); }); + }); + writeFile('testfile', 'test new content', () => {}); + }); + }); + }); + + it('fs.watchFile has been patched as eventTask', (done) => { + spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); + const zoneA = Zone.current.fork(zoneASpec); + zoneA.run(() => { + writeFile('testfile', 'test content', () => { + watchFile('testfile', {persistent: false, interval: 1000}, (curr, prev) => { + expect(curr.size).toBe(16); + expect(prev.size).toBe(12); + expect(zoneASpec.onScheduleTask).toHaveBeenCalled(); + expect(Zone.current.name).toBe('A'); + unwatchFile('testfile'); + unlink('testfile', () => { done(); }); + }); + writeFile('testfile', 'test new content', () => {}); + }); + }); + }); + }); +}); + +describe('util.promisify', () => { + it('fs.exists should work with util.promisify', (done: DoneFn) => { + const promisifyExists = util.promisify(exists); + promisifyExists(__filename) + .then( + r => { + expect(r).toBe(true); + done(); + }, + err => { fail(`should not be here with error: ${err}`); }); + }); + + it('fs.read should work with util.promisify', (done: DoneFn) => { + const promisifyRead = util.promisify(read); + const fd = openSync(__filename, 'r'); + const stats = fstatSync(fd); + const bufferSize = stats.size; + const chunkSize = 512; + const buffer = new Buffer(bufferSize); + let bytesRead = 0; + // fd, buffer, offset, length, position, callback + promisifyRead(fd, buffer, bytesRead, chunkSize, bytesRead) + .then( + (value) => { + expect(value.bytesRead).toBe(chunkSize); + closeSync(fd); + done(); + }, + err => { + closeSync(fd); + fail(`should not be here with error: ${error}.`); + }); + }); + + it('fs.write should work with util.promisify', (done: DoneFn) => { + const promisifyWrite = util.promisify(write); + const dest = __filename + 'write'; + const fd = openSync(dest, 'a'); + const stats = fstatSync(fd); + const chunkSize = 512; + const buffer = new Buffer(chunkSize); + for (let i = 0; i < chunkSize; i++) { + buffer[i] = 0; + } + // fd, buffer, offset, length, position, callback + promisifyWrite(fd, buffer, 0, chunkSize, 0) + .then( + (value) => { + expect(value.bytesWritten).toBe(chunkSize); + closeSync(fd); + unlinkSync(dest); + done(); + }, + err => { + closeSync(fd); + unlinkSync(dest); + fail(`should not be here with error: ${error}.`); + }); + }); +}); \ No newline at end of file diff --git a/packages/zone.js/test/node/http.spec.ts b/packages/zone.js/test/node/http.spec.ts new file mode 100644 index 0000000000..22022aa1ba --- /dev/null +++ b/packages/zone.js/test/node/http.spec.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +const http = require('http'); +describe('http test', () => { + it('http.request should be patched as eventTask', (done) => { + const server = http.createServer((req: any, res: any) => { res.end(); }); + server.listen(9999, () => { + const zoneASpec = { + name: 'A', + onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): + Task => { return delegate.scheduleTask(targetZone, task); } + }; + const zoneA = Zone.current.fork(zoneASpec); + spyOn(zoneASpec, 'onScheduleTask').and.callThrough(); + zoneA.run(() => { + const req = + http.request({hostname: 'localhost', port: '9999', method: 'GET'}, (res: any) => { + expect(Zone.current.name).toEqual('A'); + expect(zoneASpec.onScheduleTask).toHaveBeenCalled(); + server.close(() => { done(); }); + }); + req.end(); + }); + }); + }); +}); diff --git a/packages/zone.js/test/node/process.spec.ts b/packages/zone.js/test/node/process.spec.ts new file mode 100644 index 0000000000..cbc91a1f97 --- /dev/null +++ b/packages/zone.js/test/node/process.spec.ts @@ -0,0 +1,116 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {zoneSymbol} from '../../lib/common/utils'; + +describe('process related test', () => { + let zoneA: Zone, result: any[]; + beforeEach(() => { + zoneA = Zone.current.fork({name: 'zoneA'}); + result = []; + }); + it('process.nextTick callback should in zone', (done) => { + zoneA.run(function() { + process.nextTick(() => { + expect(Zone.current.name).toEqual('zoneA'); + done(); + }); + }); + }); + it('process.nextTick should be executed before macroTask and promise', (done) => { + zoneA.run(function() { + setTimeout(() => { result.push('timeout'); }, 0); + process.nextTick(() => { result.push('tick'); }); + setTimeout(() => { + expect(result).toEqual(['tick', 'timeout']); + done(); + }); + }); + }); + it('process.nextTick should be treated as microTask', (done) => { + let zoneTick = Zone.current.fork({ + name: 'zoneTick', + onScheduleTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task): Task => { + result.push({callback: 'scheduleTask', targetZone: targetZone.name, task: task.source}); + return parentZoneDelegate.scheduleTask(targetZone, task); + }, + onInvokeTask: (parentZoneDelegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, + task: Task, applyThis?: any, applyArgs?: any): any => { + result.push({callback: 'invokeTask', targetZone: targetZone.name, task: task.source}); + return parentZoneDelegate.invokeTask(targetZone, task, applyThis, applyArgs); + } + }); + zoneTick.run(() => { process.nextTick(() => { result.push('tick'); }); }); + setTimeout(() => { + expect(result.length).toBe(3); + expect(result[0]).toEqual( + {callback: 'scheduleTask', targetZone: 'zoneTick', task: 'process.nextTick'}); + expect(result[1]).toEqual( + {callback: 'invokeTask', targetZone: 'zoneTick', task: 'process.nextTick'}); + done(); + }); + }); + + it('should support process.on(unhandledRejection)', function(done) { + const hookSpy = jasmine.createSpy('hook'); + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.current.fork({name: 'promise'}).run(function() { + const listener = function(reason: any, promise: any) { + hookSpy(promise, reason.message); + process.removeListener('unhandledRejection', listener); + }; + process.on('unhandledRejection', listener); + const p = new Promise((resolve, reject) => { throw new Error('promise error'); }); + + setTimeout(function() { + expect(hookSpy).toHaveBeenCalledWith(p, 'promise error'); + done(); + }, 10); + }); + }); + + it('should support process.on(rejectionHandled)', function(done) { + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.current.fork({name: 'promise'}).run(function() { + const listener = function(promise: any) { + expect(promise).toEqual(p); + process.removeListener('rejectionHandled', listener); + done(); + }; + process.on('rejectionHandled', listener); + const p = new Promise((resolve, reject) => { throw new Error('promise error'); }); + + setTimeout(function() { p.catch(reason => {}); }, 10); + }); + }); + + it('should support multiple process.on(unhandledRejection)', function(done) { + const hookSpy = jasmine.createSpy('hook'); + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; + Zone.current.fork({name: 'promise'}).run(function() { + const listener1 = function(reason: any, promise: any) { + hookSpy(promise, reason.message); + process.removeListener('unhandledRejection', listener1); + }; + const listener2 = function(reason: any, promise: any) { + hookSpy(promise, reason.message); + process.removeListener('unhandledRejection', listener2); + }; + process.on('unhandledRejection', listener1); + process.on('unhandledRejection', listener2); + const p = new Promise((resolve, reject) => { throw new Error('promise error'); }); + + setTimeout(function() { + expect(hookSpy.calls.count()).toBe(2); + expect(hookSpy.calls.allArgs()).toEqual([[p, 'promise error'], [p, 'promise error']]); + done(); + }, 10); + }); + }); +}); \ No newline at end of file diff --git a/packages/zone.js/test/node/timer.spec.ts b/packages/zone.js/test/node/timer.spec.ts new file mode 100644 index 0000000000..59a9d7e0ad --- /dev/null +++ b/packages/zone.js/test/node/timer.spec.ts @@ -0,0 +1,31 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import {promisify} from 'util'; + +describe('node timer', () => { + it('util.promisify should work with setTimeout', (done: DoneFn) => { + const setTimeoutPromise = promisify(setTimeout); + setTimeoutPromise(50, 'value') + .then( + value => { + expect(value).toEqual('value'); + done(); + }, + error => { fail(`should not be here with error: ${error}.`); }); + }); + + it('util.promisify should work with setImmediate', (done: DoneFn) => { + const setImmediatePromise = promisify(setImmediate); + setImmediatePromise('value').then( + value => { + expect(value).toEqual('value'); + done(); + }, + error => { fail(`should not be here with error: ${error}.`); }); + }); +}); \ No newline at end of file diff --git a/packages/zone.js/test/node_bluebird_entry_point.ts b/packages/zone.js/test/node_bluebird_entry_point.ts new file mode 100644 index 0000000000..ae0da24027 --- /dev/null +++ b/packages/zone.js/test/node_bluebird_entry_point.ts @@ -0,0 +1,54 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Must be loaded before zone loads, so that zone can detect WTF. +import './test_fake_polyfill'; + +// Setup tests for Zone without microtask support +import '../lib/zone'; +import '../lib/common/promise'; +import '../lib/common/to-string'; +import '../lib/node/node'; +// Setup test environment +require('@bazel/jasmine').boot(); +import './test-env-setup-jasmine'; +import './wtf_mock'; + +import '../lib/zone-spec/async-test'; +import '../lib/zone-spec/fake-async-test'; +import '../lib/zone-spec/long-stack-trace'; +import '../lib/zone-spec/proxy'; +import '../lib/zone-spec/sync-test'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; + +import '../lib/testing/promise-testing'; + +const globalErrors = (jasmine as any).GlobalErrors; +const symbol = Zone.__symbol__; +if (globalErrors && !(jasmine as any)[symbol('GlobalErrors')]) { + (jasmine as any)[symbol('GlobalErrors')] = globalErrors; + (jasmine as any).GlobalErrors = function() { + const instance = new globalErrors(); + const originalInstall = instance.install; + if (originalInstall && !instance[symbol('install')]) { + instance[symbol('install')] = originalInstall; + instance.install = function() { + const originalHandlers = process.listeners('unhandledRejection'); + const r = originalInstall.apply(this, arguments); + process.removeAllListeners('unhandledRejection'); + if (originalHandlers) { + originalHandlers.forEach(h => process.on('unhandledRejection', h)); + } + return r; + }; + } + return instance; + }; +} diff --git a/packages/zone.js/test/node_entry_point.ts b/packages/zone.js/test/node_entry_point.ts new file mode 100644 index 0000000000..e63e7595d6 --- /dev/null +++ b/packages/zone.js/test/node_entry_point.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Must be loaded before zone loads, so that zone can detect WTF. +import './node-env-setup'; +import './test_fake_polyfill'; + +// Setup tests for Zone without microtask support +import '../lib/node/rollup-main'; + +require('@bazel/jasmine').boot(); +// Zone symbol prefix is set to '__zone_symbol2__' in node-env-setup.ts. +import './test-env-setup-jasmine'; +if (typeof global !== 'undefined' && + (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] !== false) { + (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] = true; +} + +import './wtf_mock'; +import '../lib/testing/zone-testing'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; +import '../lib/rxjs/rxjs-fake-async'; +import '../lib/jasmine/jasmine'; diff --git a/packages/zone.js/test/node_entry_point_no_patch_clock.ts b/packages/zone.js/test/node_entry_point_no_patch_clock.ts new file mode 100644 index 0000000000..a91766407f --- /dev/null +++ b/packages/zone.js/test/node_entry_point_no_patch_clock.ts @@ -0,0 +1,36 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Must be loaded before zone loads, so that zone can detect WTF. +import './node-env-setup'; +import './test_fake_polyfill'; + +// Setup tests for Zone without microtask support +import '../lib/node/rollup-main'; +require('@bazel/jasmine').boot(); +import './test-env-setup-jasmine-no-patch-clock'; +// Zone symbol prefix is set to '__zone_symbol2__' in node-env-setup.ts. +if (typeof global !== 'undefined' && + (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] !== false) { + (global as any)['__zone_symbol_test__fakeAsyncAutoFakeAsyncWhenClockPatched'] = true; +} + +import './wtf_mock'; +import '../lib/testing/zone-testing'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; +import '../lib/rxjs/rxjs-fake-async'; +import '../lib/jasmine/jasmine'; diff --git a/packages/zone.js/test/node_error_disable_policy_entry_point.ts b/packages/zone.js/test/node_error_disable_policy_entry_point.ts new file mode 100644 index 0000000000..841039900d --- /dev/null +++ b/packages/zone.js/test/node_error_disable_policy_entry_point.ts @@ -0,0 +1,11 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +process.env['errorpolicy'] = (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] = + 'disable'; +import './node_error_entry_point'; diff --git a/packages/zone.js/test/node_error_entry_point.ts b/packages/zone.js/test/node_error_entry_point.ts new file mode 100644 index 0000000000..5acf7f2f16 --- /dev/null +++ b/packages/zone.js/test/node_error_entry_point.ts @@ -0,0 +1,35 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +// Must be loaded before zone loads, so that zone can detect WTF. +import './test_fake_polyfill'; + +// Setup tests for Zone without microtask support +import '../lib/zone'; +import '../lib/common/promise'; +import '../lib/common/to-string'; + +process.env['errorpolicy'] = (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] = + 'disable'; +// Setup test environment +require('@bazel/jasmine').boot(); +import './test-env-setup-jasmine'; + +import './wtf_mock'; +import '../lib/common/error-rewrite'; +import '../lib/node/node'; +import '../lib/zone-spec/async-test'; +import '../lib/zone-spec/fake-async-test'; +import '../lib/zone-spec/long-stack-trace'; +import '../lib/zone-spec/proxy'; +import '../lib/zone-spec/sync-test'; +import '../lib/zone-spec/task-tracking'; +import '../lib/zone-spec/wtf'; +import '../lib/rxjs/rxjs'; + +import '../lib/testing/promise-testing'; diff --git a/packages/zone.js/test/node_error_lazy_policy_entry_point.ts b/packages/zone.js/test/node_error_lazy_policy_entry_point.ts new file mode 100644 index 0000000000..61b5e56093 --- /dev/null +++ b/packages/zone.js/test/node_error_lazy_policy_entry_point.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +process.env['errorpolicy'] = (global as any)['__Zone_Error_BlacklistedStackFrames_policy'] = 'lazy'; +import './node_error_entry_point'; diff --git a/packages/zone.js/test/node_tests.ts b/packages/zone.js/test/node_tests.ts new file mode 100644 index 0000000000..1366ac573d --- /dev/null +++ b/packages/zone.js/test/node_tests.ts @@ -0,0 +1,16 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import './node/events.spec'; +import './node/fs.spec'; +import './node/process.spec'; +import './node/Error.spec'; +import './node/crypto.spec'; +import './node/http.spec'; +import './node/console.spec'; +import './node/timer.spec'; diff --git a/packages/zone.js/test/npm_package/npm_package.spec.ts b/packages/zone.js/test/npm_package/npm_package.spec.ts new file mode 100644 index 0000000000..1c9de06332 --- /dev/null +++ b/packages/zone.js/test/npm_package/npm_package.spec.ts @@ -0,0 +1,143 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +import * as path from 'path'; +import * as shx from 'shelljs'; + +describe('Zone.js npm_package', () => { + beforeEach( + () => {shx.cd( + path.dirname(require.resolve('angular/packages/zone.js/npm_package/package.json')))}); + describe('misc root files', () => { + describe('README.md', () => { + it('should have a README.md file with basic info', + () => { expect(shx.cat('README.md')).toContain(`Zone`); }); + }); + }); + + describe('primary entry-point', () => { + const packageJson = 'package.json'; + + it('should have a package.json file', + () => { expect(shx.grep('"name":', packageJson)).toContain(`zone.js`); }); + + it('should contain correct version number with the PLACEHOLDER string replaced', () => { + expect(shx.grep('"version":', packageJson)).toMatch(/\d+\.\d+\.\d+(?!-PLACEHOLDER)/); + }); + + it('should contain module resolution mappings', + () => { expect(shx.grep('"main":', packageJson)).toContain(`dist/zone-node.js`); }); + }); + + describe('check dist folder', () => { + beforeEach(() => { shx.cd('./dist'); }); + afterEach(() => { shx.cd('../'); }); + describe('typescript support', () => { + it('should have an zone.js.d.ts file', + () => { expect(shx.cat('zone.js.d.ts')).toContain('declare const'); }); + }); + + describe('closure', () => { + it('should contain externs', + () => { expect(shx.cat('zone_externs.js')).toContain('Externs for zone.js'); }); + }); + + describe('es5', () => { + it('zone.js(es5) should not contain es6 spread code', + () => { expect(shx.cat('zone.js')).not.toContain('let value of values'); }); + }); + + describe('es2015', () => { + it('zone-evergreen.js(es2015) should contain es6 code', + () => { expect(shx.cat('zone-evergreen.js')).toContain('let value of values'); }); + }); + + describe('dist file list', () => { + it('should contain all files', () => { + const list = shx.ls('./').stdout.split('\n').sort().slice(1); + const expected = [ + 'async-test.js', + 'async-test.min.js', + 'fake-async-test.js', + 'fake-async-test.min.js', + 'jasmine-patch.js', + 'jasmine-patch.min.js', + 'long-stack-trace-zone.js', + 'long-stack-trace-zone.min.js', + 'mocha-patch.js', + 'mocha-patch.min.js', + 'proxy.js', + 'proxy.min.js', + 'sync-test.js', + 'sync-test.min.js', + 'task-tracking.js', + 'task-tracking.min.js', + 'webapis-media-query.js', + 'webapis-media-query.min.js', + 'webapis-notification.js', + 'webapis-notification.min.js', + 'webapis-rtc-peer-connection.js', + 'webapis-rtc-peer-connection.min.js', + 'webapis-shadydom.js', + 'webapis-shadydom.min.js', + 'wtf.js', + 'wtf.min.js', + 'zone_externs.js', + 'zone-bluebird.js', + 'zone-bluebird.min.js', + 'zone-error.js', + 'zone-error.min.js', + 'zone-evergreen.js', + 'zone-evergreen.min.js', + 'zone-evergreen-testing-bundle.js', + 'zone-evergreen-testing-bundle.min.js', + 'zone-legacy.js', + 'zone-legacy.min.js', + 'zone-mix.js', + 'zone-mix.min.js', + 'zone-node.js', + 'zone-node.min.js', + 'zone-patch-canvas.js', + 'zone-patch-canvas.min.js', + 'zone-patch-cordova.js', + 'zone-patch-cordova.min.js', + 'zone-patch-electron.js', + 'zone-patch-electron.min.js', + 'zone-patch-fetch.js', + 'zone-patch-fetch.min.js', + 'zone-patch-jsonp.js', + 'zone-patch-jsonp.min.js', + 'zone-patch-promise-test.js', + 'zone-patch-promise-test.min.js', + 'zone-patch-resize-observer.js', + 'zone-patch-resize-observer.min.js', + 'zone-patch-rxjs-fake-async.js', + 'zone-patch-rxjs-fake-async.min.js', + 'zone-patch-rxjs.js', + 'zone-patch-rxjs.min.js', + 'zone-patch-socket-io.js', + 'zone-patch-socket-io.min.js', + 'zone-patch-user-media.js', + 'zone-patch-user-media.min.js', + 'zone-testing-bundle.js', + 'zone-testing-bundle.min.js', + 'zone-testing-node-bundle.js', + 'zone-testing-node-bundle.min.js', + 'zone-testing.js', + 'zone-testing.min.js', + 'zone.js', + 'zone.js.d.ts', + 'zone.min.js', + ].sort(); + expect(list.length).toBe(expected.length); + for (let i = 0; i < list.length; i++) { + expect(list[i]).toEqual(expected[i]); + } + }); + }); + }); +}); diff --git a/packages/zone.js/test/patch/IndexedDB.spec.js b/packages/zone.js/test/patch/IndexedDB.spec.js new file mode 100644 index 0000000000..f563de0997 --- /dev/null +++ b/packages/zone.js/test/patch/IndexedDB.spec.js @@ -0,0 +1,133 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +'use strict'; + +describe( + 'IndexedDB', ifEnvSupports('IDBDatabase', function() { + var testZone = zone.fork(); + var db; + + beforeEach(function(done) { + var openRequest = indexedDB.open('_zone_testdb'); + openRequest.onupgradeneeded = function(event) { + db = event.target.result; + var objectStore = db.createObjectStore('test-object-store', {keyPath: 'key'}); + objectStore.createIndex('key', 'key', {unique: true}); + objectStore.createIndex('data', 'data', {unique: false}); + + objectStore.transaction.oncomplete = function() { + var testStore = + db.transaction('test-object-store', 'readwrite').objectStore('test-object-store'); + testStore.add({key: 1, data: 'Test data'}); + testStore.transaction.oncomplete = function() { done(); } + }; + }; + }); + + afterEach(function(done) { + db.close(); + + var openRequest = indexedDB.deleteDatabase('_zone_testdb'); + openRequest.onsuccess = function(event) { done(); }; + }); + + describe('IDBRequest', function() { + it('should bind EventTarget.addEventListener', function(done) { + testZone.run(function() { + db.transaction('test-object-store') + .objectStore('test-object-store') + .get(1) + .addEventListener('success', function(event) { + expect(zone).toBeDirectChildOf(testZone); + expect(event.target.result.data).toBe('Test data'); + done(); + }); + }); + }); + + it('should bind onEventType listeners', function(done) { + testZone.run(function() { + db.transaction('test-object-store').objectStore('test-object-store').get(1).onsuccess = + function(event) { + expect(zone).toBeDirectChildOf(testZone); + expect(event.target.result.data).toBe('Test data'); + done(); + }; + }); + }); + }); + + describe('IDBCursor', function() { + it('should bind EventTarget.addEventListener', function(done) { + testZone.run(function() { + db.transaction('test-object-store') + .objectStore('test-object-store') + .openCursor() + .addEventListener('success', function(event) { + var cursor = event.target.result; + if (cursor) { + expect(zone).toBeDirectChildOf(testZone); + expect(cursor.value.data).toBe('Test data'); + done(); + } else { + throw 'Error while reading cursor!'; + } + }); + }); + }); + + it('should bind onEventType listeners', function(done) { + testZone.run(function() { + db.transaction('test-object-store') + .objectStore('test-object-store') + .openCursor() + .onsuccess = function(event) { + var cursor = event.target.result; + if (cursor) { + expect(zone).toBeDirectChildOf(testZone); + expect(cursor.value.data).toBe('Test data'); + done(); + } else { + throw 'Error while reading cursor!'; + } + }; + }); + }); + }); + + describe('IDBIndex', function() { + it('should bind EventTarget.addEventListener', function(done) { + testZone.run(function() { + db.transaction('test-object-store') + .objectStore('test-object-store') + .index('data') + .get('Test data') + .addEventListener('success', function(event) { + expect(zone).toBeDirectChildOf(testZone); + expect(event.target.result.key).toBe(1); + done(); + }); + }); + }); + + it('should bind onEventType listeners', function(done) { + testZone.run(function() { + db.transaction('test-object-store') + .objectStore('test-object-store') + .index('data') + .get('Test data') + .onsuccess = function(event) { + expect(zone).toBeDirectChildOf(testZone); + expect(event.target.result.key).toBe(1); + done(); + }; + }); + }); + }); + })); \ No newline at end of file diff --git a/packages/zone.js/test/performance/eventTarget.js b/packages/zone.js/test/performance/eventTarget.js new file mode 100644 index 0000000000..b8e56dd122 --- /dev/null +++ b/packages/zone.js/test/performance/eventTarget.js @@ -0,0 +1,80 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +(function(_global) { + var testRunner = _global['__zone_symbol__testRunner']; + var mark = _global['__zone_symbol__mark']; + var measure = _global['__zone_symbol__measure']; + var zone = _global['__zone_symbol__callbackZone']; + var button; + var testTarget = { + title: 'addEventListener', + times: 10, + before: function() { + button = document.createElement('button'); + document.body.appendChild(button); + _global['__zone_symbol__callbackContext'].measureName = 'addEventListener_callback'; + _global['__zone_symbol__callbackContext'].type = 'eventTask'; + _global['__zone_symbol__callbackContext'].source = 'addEventListener'; + }, + after: function() { + document.body.removeChild(button); + button = null; + }, + apis: [ + { + supportClear: true, + method: 'addEventListener', + nativeMethod: '__zone_symbol__addEventListener', + clearMethod: 'removeEventListener', + nativeClearMethod: '__zone_symbol__removeEventListener', + run: function() { + var listener = function() {}; + button.addEventListener('click', listener); + return listener; + }, + runClear: function(timerId) { return button.removeEventListener('click', timerId); }, + nativeRun: function() { + var listener = function() {}; + button['__zone_symbol__addEventListener']('click', listener); + return listener; + }, + nativeRunClear: function(timerId) { + return button['__zone_symbol__removeEventListener']('click', timerId); + } + }, + { + isCallback: true, + supportClear: false, + method: 'addEventListener_callback', + nativeMethod: 'native_addEventListener_callback', + run: function() { + var listener = function() {}; + zone.run(function() { button.addEventListener('click', listener); }); + var event = document.createEvent('Event'); + event.initEvent('click', true, true); + button.dispatchEvent(event); + button.removeEventListener('click', listener); + }, + nativeRun: function() { + var func = function() {}; + var listener = function() { + mark('native_addEventListener_callback'); + func.apply(this, arguments); + measure('native_addEventListener_callback', 'native_addEventListener_callback'); + }; + button['__zone_symbol__addEventListener']('click', listener); + var event = document.createEvent('Event'); + event.initEvent('click', true, true); + button.dispatchEvent(event); + button['__zone_symbol__removeEventListener']('click', listener); + } + } + ], + }; + return testRunner(testTarget); +}(typeof window === 'undefined' ? global : window)); diff --git a/packages/zone.js/test/performance/performance.html b/packages/zone.js/test/performance/performance.html new file mode 100644 index 0000000000..37d5b59431 --- /dev/null +++ b/packages/zone.js/test/performance/performance.html @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + +
Performance Bencnhmark of Zone.js vs Native Delegate!
+
+
+ + + + + + +
+ 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=