Compare commits

..

401 Commits

Author SHA1 Message Date
Trotyl 5c89d6bffa feat(core): support metadata reflection for native class types (#22356)
closes #21731

PR Close #22356
2018-02-21 16:09:27 -08:00
Philip Pham 3e6a86fb0a fix(forms): set state before emitting a value from ngModelChange (#21514)
Closes #21513.

PR Close #21514
2018-02-21 15:59:33 -08:00
Trotyl a7ebf5aadd fix(core): properly handle function without prototype in reflector (#22284)
closes #19978

PR Close #22284
2018-02-21 14:52:04 -08:00
Marco Alvarado b42921bbd2 docs(aio): updates directive event hooks real capabilities (#16654)
Minor documentation update to include event hooks that were assumed to only work on components.

Closes angular/angular#10221

PR Close #16654
2018-02-21 14:51:04 -08:00
zhilich 722dec11b0 docs(aio): Wrong code example. Form status field was added later in the guide. (#21275)
PR Close #21275
2018-02-21 11:06:47 -08:00
Ricardo M 9e6268ba59 docs(http): fix a typo in code comment (#22327)
PR Close #22327
2018-02-21 11:06:06 -08:00
Victor Berchet 435f6eecd2 build: make git revert messages valid (#22339)
`git revert` default message is "Revert <original message>" (no semi-colon)

PR Close #22339
2018-02-21 11:05:35 -08:00
Kapunahele Wong 1c1cbba04b docs: add ngStyle to cheat sheet (#22070)
PR Close #22070
2018-02-20 16:08:15 -08:00
Ro Savage 3b692a55a7 docs(aio): fix incorrect quote mark usage (#22335)
PR Close #22335
2018-02-20 15:42:55 -08:00
Oussama Ben Brahim 69a0578e00 docs(aio): fix the css of the heroes component's buttons (#22333)
Fixes #22222

PR Close #22333
2018-02-20 15:41:56 -08:00
Pascal Zwick b5ca275590 docs(aio): Fix name of component (#22332)
PR Close #22332
2018-02-20 15:41:35 -08:00
Stephen Fluin 519f022b02 docs(aio): update installed mobile tool list (#22331)
PR Close #22331
2018-02-20 15:41:15 -08:00
Alex Eagle 236a9320df build: update tsickle dep from compiler-cli (#22295)
PR Close #22295
2018-02-20 15:40:44 -08:00
Alex Eagle 28ac24444f fix(compiler-cli): add missing entry point to package, update tsickle (#22295)
PR Close #22295
2018-02-20 15:40:44 -08:00
Chuck Jazdzewski 99909bbf2c feat(ivy): generate pipe references and definitions (#22034)
PR Close #22034
2018-02-20 13:58:03 -08:00
Marc Laval ee60bb5b36 fix(ivy): pureFunction8 should update the right bindings (#22313)
PR Close #22313
2018-02-20 11:36:50 -08:00
Jeff Cross f6120c09e7 docs(aio): add Nx and Angular Enterprise Playbook to resources (#22321)
PR Close #22321
2018-02-20 10:09:33 -08:00
Frederik Prijck e2bdef4cf6 test(language-service): fix minor typos (#21372)
PR Close #21372
2018-02-20 10:08:55 -08:00
Victor Berchet 8115edc82f fix(common): then and else template might be set to null (#22298)
PR Close #22298
2018-02-18 19:25:28 -08:00
Victor Berchet a8b5465e24 fix(ivy): update master with renamings (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
Victor Berchet 9ce495b3d8 refactor(ivy): simplify interpolation instructions (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
Victor Berchet d40263447d refactor(ivy): move get functions next to their underlying variable (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
Victor Berchet 31c5c1060a docs(ivy): update the API docs instructions to add details about removing attributes (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
Victor Berchet c9ebd60435 refactor(ivy): interpolatiom instructions do not support NO_CHANGE at input. (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
Victor Berchet 5a14e2238f refactor(ivy): split the memory instruction into store and load (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
WilliamKoza 3ceee99e22 feat(compiler-cli): Check unvalidated combination of ngc and TypeScript (#22293)
closes #20669

PR Close #22293
2018-02-18 15:12:46 -08:00
Johannes Hoppe 28b23f954c docs(aio): add angular-buch to resources (#22163)
adds a link to the website of our book. second version of the text. thanks!

PR Close #22163
2018-02-18 15:12:13 -08:00
Johannes Hoppe ad17e5e791 docs(aio): add angular-buch to resources (#22163)
adds a link to the website of our book. many thanks for reviewing this

PR Close #22163
2018-02-18 15:12:13 -08:00
Victor Berchet c30d329faa fix(ivy): fix merge errors (master is broken) (#22291)
PR Close #22291
2018-02-18 15:03:39 -08:00
Trotyl 991300b86c feat(platform-browser): do not throw error when Hammer.js not loaded (#22257)
closes #16992

PR Close #22257
2018-02-18 13:29:14 -08:00
Derk Snijders - i3anaan 7c45db3a19 docs: correct grammar mistakes in CONTRIBUTING.md (#22285)
Various grammar mistakes were present in the contribution guidelines
This commit corrects some of them

PR Close #22285
2018-02-18 13:27:23 -08:00
Trotyl 67cf11d071 feat(common): better error message when non-template element used in NgIf (#22274)
closes #16410

PR Close #22274
2018-02-18 13:26:50 -08:00
Kara Erickson 49082d7ab2 feat(ivy): support host attributes (#22213)
PR Close #22213
2018-02-18 13:22:38 -08:00
Kara Erickson 6b627f67db test(ivy): add missing host listener and host attribute binding tests (#22213)
PR Close #22213
2018-02-18 13:22:38 -08:00
Kara Erickson 5c320b4c2a test(ivy): add missing host binding and query tests (#22213)
PR Close #22213
2018-02-18 13:22:34 -08:00
Misko Hevery ac2b04a5ab test(ivy): Add small_app spec for sprint #3 (#22018)
PR Close #22018
2018-02-18 13:18:54 -08:00
Misko Hevery a63b764b54 test(ivy): move compiler canonical specs into a single directory (#22018)
PR Close #22018
2018-02-18 13:18:54 -08:00
Miško Hevery 2654357c72 docs: update BAZEL.md crosstool error instructions (#22018)
PR Close #22018
2018-02-18 13:18:54 -08:00
George Kalpakas 4ec40c6ab2 fix(aio): improve announcement-bar layout with wide logos (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
Joe Eames 80d424798e docs(aio): added ngconf announcement (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
Victor Berchet 7fa2d4b503 fix: merge-pr script (#22290)
PR Close #22290
2018-02-18 13:13:29 -08:00
Victor Berchet f4845fae12 build: use authenticated mode for the merge script (#22269)
`TOKEN` is the name with use for other GH scripts

PR Close #22269
2018-02-18 12:44:42 -08:00
Kara Erickson f693be3996 feat(ivy): add pureFunction0 instruction (#22214)
PR Close #22214
2018-02-16 18:03:55 -08:00
Kara Erickson a73d5308e0 refactor(ivy): rename objectLiteral to pureFn to prep for pipes (#22214)
PR Close #22214
2018-02-16 18:03:55 -08:00
Matias Niemelä e1bf067090 fix(animations): report correct totalTime value even during noOp animations (#22225)
This patch ensures that if the NoopAnimationsModule is used then it will
correctly report the associated `totalTime` property within the emitted
AnimationEvent instance when an animation event trigger is fired.

BREAKING CHANGE: When animation is trigged within a disabled zone, the
associated event (which an instance of AnimationEvent) will no longer
report the totalTime as 0 (it will emit the actual time of the
animation). To detect if an animation event is reporting a disabled
animation then the `event.disabled` property can be used instead.

PR Close #22225
2018-02-16 18:03:31 -08:00
Fabian Wiles 884de18cba docs: replace plnkr with StackBlitz (#20365)
PR Close #20365
2018-02-16 15:12:10 -08:00
Marc Laval dfa2fb95d5 test(ivy): clean the imported renderer2 (#22255)
PR Close #22255
2018-02-16 15:11:23 -08:00
Trotyl Yu 2639b4bffb fix(common): correct mapping of Observable methods (#20518)
fixes #20516
PR Close #20518
2018-02-16 15:10:31 -08:00
George Kalpakas 978f97cc59 test(aio): increase docs-test timeouts to prevent flakes on Travis (#22261)
PR Close #22261
2018-02-16 14:46:23 -08:00
Oussama Ben Brahim f1a063298e feat(core): set preserveWhitespaces to false by default (#22046)
Fixes #22027

PR Close #22046
2018-02-16 09:06:14 -08:00
Victor Berchet d241532488 docs(ivy): add a note about exporting top level variables (#22234)
PR Close #22234
2018-02-15 16:16:40 -08:00
Alex Rickabaugh f755db78dc fix(core): require factory to be provided for shakeable InjectionToken (#22207)
InjectionToken can be created with an ngInjectableDef, and previously
this allowed the full expressiveness of @Injectable. However, this
requires a runtime reflection system in order to generate factories
from expressed provider declarations.

Instead, this change requires scoped InjectionTokens to provide the
factory directly (likely using inject() for the arguments), bypassing
the need for a reflection system.

Fixes #22205

PR Close #22207
2018-02-15 16:16:16 -08:00
Victor Berchet 5dd2b5135d refactor(ivy): rename bindX() functions to interpolationX() (#22229)
rationale: remove the confusion with `bind()` and `bind0()`

PR Close #22229
2018-02-15 14:20:53 -08:00
Alex Rickabaugh 7ac34e42a0 feat: allow direct scoping of @Injectables to the root injector (#22185)
@Injectable() supports a scope parameter which specifies the target module.
However, it's still difficult to specify that a particular service belongs
in the root injector. A developer attempting to ensure that must either
also provide a module intended for placement in the root injector or target
a module known to already be in the root injector (e.g. BrowserModule).
Both of these strategies are cumbersome and brittle.

Instead, this commit adds a token APP_ROOT_SCOPE which provides a
straightforward way of targeting the root injector directly, without
requiring special knowledge of modules within it.

PR Close #22185
2018-02-15 14:20:27 -08:00
Alex Eagle 029dbf0e18 feat(bazel): ng_module produces bundle index (#22176)
It creates the bundle index .d.ts and .metadata.json files.
The names are based on the ng_module target.

PR Close #22176
2018-02-15 14:08:53 -08:00
Alex Eagle bba65e0f41 feat(bazel): introduce a binary stamping feature (#22176)
This grabs version control metadata and makes it available in the build, eg. to put in the version field for released artifacts

PR Close #22176
2018-02-15 14:08:53 -08:00
Alex Eagle a069e08354 refactor(bazel): convert most ts_library to ng_module (#22176)
This is necessary so we can produce ng metadata for our packages that are published as libraries

PR Close #22176
2018-02-15 14:08:53 -08:00
Matias Niemelä 03d93c96a3 Revert: "build: merge-pr new checks that all requested changes have been addressed (#21817)"
This reverts commit 4a4d749710.
2018-02-15 11:00:52 -08:00
Adrien Boullé 020338230f style: fix typos boostrap to bootstrap (#21917)
PR Close #21917
2018-02-15 09:54:00 -08:00
Victor Berchet a1d86daa71 refactor(ivy): assertion (#22189)
Encourage the use of message to explain the assertion

PR Close #22189
2018-02-15 09:53:05 -08:00
Stefanie Fluin 7078fbffb4 fix(aio): improve printing styles (#19651)
printfix

PR Close #19651
2018-02-15 09:52:32 -08:00
Matias Niemelä 0aa9b46b79 Revert "build: allow bazel build ... (#22168)"
This reverts commit 265ac8a106.
2018-02-15 03:28:35 -08:00
Matias Niemelä 831592c381 Revert "style: fix typos boostrap to bootstrap (#21917)"
This reverts commit 363498b6b4.
2018-02-14 22:56:44 -05:00
Matias Niemelä f628797d91 Revert "refactor(ivy): assertion (#22189)"
This reverts commit 0b683123d2.
2018-02-14 22:56:11 -05:00
Matias Niemelä 47f51c2ead Revert "Revert "build: merge-pr new checks that all requested changes have been addressed (#21817)""
This reverts commit 5b8eb9c5c7.
2018-02-14 22:55:56 -05:00
Matias Niemelä ba9cd5bbc4 Revert "fix(aio): improve printing styles (#19651)"
This reverts commit b54ad053f9.
2018-02-14 22:55:24 -05:00
Stefanie Fluin b54ad053f9 fix(aio): improve printing styles (#19651)
printfix

PR Close #19651
2018-02-14 18:49:58 -05:00
Matias Niemelä 5b8eb9c5c7 Revert "build: merge-pr new checks that all requested changes have been addressed (#21817)"
This reverts commit 4a4d749710.
2018-02-14 18:48:45 -05:00
Victor Berchet 0b683123d2 refactor(ivy): assertion (#22189)
Encourage the use of message to explain the assertion

PR Close #22189
2018-02-14 18:42:04 -05:00
Adrien Boullé 363498b6b4 style: fix typos boostrap to bootstrap (#21917)
PR Close #21917
2018-02-14 18:21:52 -05:00
Olivier Combe a1bb56f739 docs: fix changelog errors (#22226)
PR Close #22226
2018-02-14 18:18:22 -05:00
Matias Niemelä 5bb9fcad3e build: comment-out chromium version checking code temporarily (#22232)
Related #22231

PR Close #22232
2018-02-14 17:26:43 -05:00
Pat f4697f351e docs: typo - components should be possessive (#22172)
PR Close #22172
2018-02-14 15:06:52 -05:00
JiaLi.Passion 1d571b299d feat(platform-browser): fix #19604, can config hammerOptions (#21979)
PR Close #21979
2018-02-14 15:02:58 -05:00
Kapunahele Wong 3a0b5a928c docs(aio): fix extraneous divs (#22069)
PR Close #22069
2018-02-14 15:02:36 -05:00
Alex Eagle 265ac8a106 build: allow bazel build ... (#22168)
Note, the reason this commit removes `firebase-tools` is:

1) firebase-tools has an optional dependency on
https://www.npmjs.com/package/@google-cloud/functions-emulator
2) yarn's `--ignore-optional` doesn't work for transitive deps, so
there's no way to yarn install without getting that functions-emulator
package
3) functions-emulator has a transitive dep on `grpc`
4) the version of `grpc` we get has `BUILD` files and no `WORKSPACE`
file so it always breaks `bazel build ...`

It could be solved by any of:
1) remove firebase-tools - this is what I did
2) fix yarn so you can omit optional deps of a transitive dep
3) make functions-emulator depend transitively on a more recent `grpc`
version
4) patch `grpc` after install by doing an `rm` command in our
postinstall or something

In its place we must install protobufjs. This is needed by the
ngc-wrapped test, which needs jasmine as well as bazel's worker mode
dependencies, and therefore cannot simply rely on
node_modules =
"@build_bazel_rules_typescript_tsc_wrapped_deps//:node_modules"

PR Close #22168
2018-02-14 15:01:41 -05:00
Misko Hevery fa7d8907d0 docs: add changelog for 6.0.0-beta.4 2018-02-13 21:35:43 -08:00
Misko Hevery 0220ce7002 docs: add changelog for 5.2.5 2018-02-13 21:34:04 -08:00
Misko Hevery 3bd0b2ab28 release: cut the 6.0.0-beta.4 release 2018-02-13 21:31:14 -08:00
Kara Erickson a589ca0adb test(ivy): clean up canonical spec (#22188)
PR Close #22188
2018-02-13 13:04:43 -08:00
Trotyl 72f8abd7b3 fix(compiler): make unary plus operator consistent to JavaScript (#22154)
fixes #22089

PR Close #22154
2018-02-13 13:04:30 -08:00
Miško Hevery 20a900b648 test: Add bundle symbol extractor tool (#22002)
This tool will be used for extracting symbols out of bundles so that
we can assert that only whitelisted symbols are allowed.

PR Close #22002
2018-02-13 11:28:54 -08:00
adasilva 6435ecd3c6 fix(platform-browser): support 0/false/null values in transfer_state (#22179)
Issue #22178

PR Close #22179
2018-02-13 11:28:22 -08:00
guillaumebarranco 16d1700a8e fix(core): add stacktrace in log when error during cleanup component in TestBed (#22162)
PR Close #22162
2018-02-13 11:28:08 -08:00
Olivier Combe b75cf3f70b ci: update ngbot config file (#22173)
Fixes #22053
PR Close #22173
2018-02-13 10:26:06 -08:00
Alex Eagle 4f19491fec ci: remove conditional clause for bazel install (#22170)
No longer needed since we don't have a bazel job
PR Close #22170
2018-02-13 10:25:51 -08:00
Alex Eagle 8f36fd1374 ci: remove bazel job from Travis (#22170)
This saves us an executor on Travis.

Note that we still do a bazel build on travis when we run the integration tests under e2e_2.

We expect that CircleCI is the only place we'll ever consume bazel-built artifacts.

PR Close #22170
2018-02-13 10:25:51 -08:00
Marc Laval 2de0d4c1db perf(ivy): use official build optimizer rollup plugin in int test (#22121)
PR Close #22121
2018-02-13 10:25:37 -08:00
Kara 5e4af7c550 fix(ivy): o2+ should work with multiple template instances (#22075)
Closes #22075
2018-02-13 10:24:41 -08:00
Alex Eagle 8ec21fc325 ci: enable bazel remote caching on CircleCI (#21784)
This should cause Bazel builds to be incremental, only re-building parts of Angular affected by changes since the last build.
It also fixes a potential version skew, where CI was running the Bazel linter binaries in the ngcontainer docker image, but developers built them using the versions in WORKSPACE

PR Close #21784
2018-02-13 10:10:41 -08:00
Oussama Ben Brahim eb48750705 docs(aio): fix typo in "preserveWhitespaces" example (#22182)
Fixes #22147

PR Close #22182
2018-02-12 15:57:42 -08:00
Chuck Jazdzewski be59c3a98c fix(common): weaken AsyncPipe transform signature (#22169)
The AsyncPipe type signature was changed to allow
deferred creation of promises and observalbes that
is supported by the implementation by allowing
`Promise<T>|null|undefined` and by allowing
`Observable<T>|null|undefined`.

PR Close #22169
2018-02-12 15:57:29 -08:00
Chuck Jazdzewski b333919722 build(bazel): allow ng_modules to elide .ngsummary.closure.js files (#22107)
PR Close #22107
2018-02-12 15:57:17 -08:00
Alex Rickabaugh 235a235fab feat: change @Injectable() to support tree-shakeable tokens (#22005)
This commit bundles 3 important changes, with the goal of enabling tree-shaking
of services which are never injected. Ordinarily, this tree-shaking is prevented
by the existence of a hard dependency on the service by the module in which it
is declared.

Firstly, @Injectable() is modified to accept a 'scope' parameter, which points
to an @NgModule(). This reverses the dependency edge, permitting the module to
not depend on the service which it "provides".

Secondly, the runtime is modified to understand the new relationship created
above. When a module receives a request to inject a token, and cannot find that
token in its list of providers, it will then look at the token for a special
ngInjectableDef field which indicates which module the token is scoped to. If
that module happens to be in the injector, it will behave as if the token
itself was in the injector to begin with.

Thirdly, the compiler is modified to read the @Injectable() metadata and to
generate the special ngInjectableDef field as part of TS compilation, using the
PartialModules system.

Additionally, this commit adds several unit and integration tests of various
flavors to test this change.

PR Close #22005
2018-02-12 14:34:59 -08:00
Alex Rickabaugh 2d5e7d1b52 feat(compiler): mark @NgModules in provider lists for identification at runtime (#22005)
All of the providers in a module get compiled into a module definition in the
factory file. Some of these providers are for the actual module types, as those
are available for injection in Angular. For tree-shakeable tokens, the runtime
needs to be able to distinguish which modules are present in an injector.

This change adds a NodeFlag which tags those module providers for later
identification.

PR Close #22005
2018-02-12 14:34:59 -08:00
Alex Rickabaugh 647b8595d0 build: update some ts_library rules to ng_module (#22005)
This is needed so the rules produce metadata.json files, which is essential
for building compiler/integration tests with Bazel.

PR Close #22005
2018-02-12 14:34:59 -08:00
Alex Rickabaugh 0a1a397cd7 fix(platform-browser): add @Injectable where it was missing (#22005)
PR Close #22005
2018-02-12 14:34:59 -08:00
mgmarlow 7f9b1b78f6 docs(aio): add angular-playground to resources (#22042)
PR Close #22042
2018-02-12 14:30:58 -08:00
Johannes Hoppe 1e9484673d docs(aio): add angular.schule to resources (#22164)
adds a link to our website. many thanks for reviewing this

PR Close #22164
2018-02-12 10:01:23 -08:00
Suguru Inatomi 88bec238ac fix(aio): remove broken span closing tag (#22146)
PR Close #22146
2018-02-12 10:01:10 -08:00
Victor Berchet 62e7b9da1e refactor(ivy): code simplification (#22082)
PR Close #22082
2018-02-12 10:00:56 -08:00
Victor Berchet 61341b2791 refactor(ivy): generatePropertyAliases (#22082)
PR Close #22082
2018-02-12 10:00:56 -08:00
Dan Bucholtz 92a5876f51 refactor(router): move activation to private method (#22033)
PR Close #22033
2018-02-12 10:00:36 -08:00
yerkebulan a57df4ee20 docs(aio): put structural directives back in the nav (#21856)
PR Close #21856
2018-02-12 10:00:14 -08:00
Misko Hevery 92d7060cb0 Revert "build(bazel): allow ng_modules to elide .ngsummary.closure.js files (#22107)"
This reverts commit 263a2eca88.
2018-02-09 20:08:41 -08:00
Alex Eagle 7e9b120452 build: update to latest bazel rules (#22127)
PR Close #22127
2018-02-09 17:21:54 -08:00
Alex Eagle b081dfe705 fix(bazel): allow TS to read ambient typings (#21876)
Same fix as https://github.com/bazelbuild/rules_typescript/commit/e70d7a2a7c08d7031f4dd4a385a7a6aa27296948
This is because the CompilerOptions needs to have directoryExists undefined in order to get the google3 behavior,
so we have to set the property outside the constructor.

Fixes #21872

PR Close #21876
2018-02-09 17:16:25 -08:00
Miško Hevery 4a4d749710 build: merge-pr new checks that all requested changes have been addressed (#21817)
PR Close #21817
2018-02-09 17:14:17 -08:00
Victor Berchet c878d55397 docs: add VSCode interaction issue to bazel docs (#22128)
PR Close #22128
2018-02-09 17:13:06 -08:00
Chuck Jazdzewski 263a2eca88 build(bazel): allow ng_modules to elide .ngsummary.closure.js files (#22107)
PR Close #22107
2018-02-09 16:07:49 -08:00
Olivier Combe 44154e71fd fix(common): round currencies based on decimal digits in CurrencyPipe (#21783)
By default, we now round currencies based on the number of decimal digits available for that currency instead of using the rouding defined in the number formats.
More info about that can be found in http://www.unicode.org/cldr/charts/latest/supplemental/detailed_territory_currency_information.html#format_info

Fixes #10189

PR Close #21783
2018-02-09 14:42:23 -08:00
Olivier Combe 0b2f7d13d0 fix(common): regenerate i18n locale data files (#21783)
PR Close #21783
2018-02-09 14:42:23 -08:00
Olivier Combe 420cc7afc6 fix(common): add locale currency values (#21783)
we now use locale currency symbols, since they may be different in each locale (we were only using english data previously)

Fixes #20385

PR Close #21783
2018-02-09 14:42:23 -08:00
Pete Bacon Darwin 5fc77c90cb fix(aio): do not rewrite /styleguide URL in Service Worker (#22085)
This URL needs to be redirected via the server, so
we must exclude it from being rewitten.

Closes #22078

PR Close #22085
2018-02-09 13:10:35 -08:00
Mikhail Burshteyn c3484450b8 docs: fix typo in http.md (#22058)
PR Close #22058
2018-02-09 13:10:23 -08:00
Pete Bacon Darwin fbef94a8ee feat(aio): enable data driven homepage announcements (#22043)
PR Close #22043
2018-02-09 13:10:11 -08:00
Chuck Jazdzewski aa456edafc refactor(ivy): validate that identifier identity in emitted output (#21877)
Modifies validation syntax to generate back references to ensure
that identifiers are used consistently.

Introduced … to allow validating constant definition and usage.

PR Close #21877
2018-02-09 13:06:10 -08:00
Pete Bacon Darwin 7007f51c35 feat(aio): first pass API docs redesign (#21874)
Includes:

* display ToC for API docs
* update dgeni-packages to 0.24.1
* add floating sidebar in API docs
* add breadcrumbs and structured data for Google crawler
* improved rendering of method overloads
* properties rendered in a table
* params rendered with docs
* removal of outdated "infobox" from all API docs

PR Close #21874
2018-02-09 13:05:16 -08:00
Trotyl bc1e22922a docs(aio): several fix for ngmodule guides (#21517)
PR Close #21517
2018-02-09 13:03:47 -08:00
Miško Hevery cf8d512e43 Revert "fix(forms): set state before emitting a value from ngModelChange (#21514)"
This reverts commit 9744a1c966.
2018-02-09 10:11:23 -08:00
Miško Hevery 0b1f5d2127 Revert "docs(common): add HttpParamsOptions to the public API (#20332)"
This reverts commit a9545aba4d.
2018-02-08 14:37:27 -08:00
Alex Eagle dcf64a0d01 fix(bazel): improve error message for missing assets (#22096)
fixes #22095

PR Close #22096
2018-02-08 10:01:27 -08:00
George Kalpakas a9545aba4d docs(common): add HttpParamsOptions to the public API (#20332)
Fixes #20276

PR Close #20332
2018-02-08 09:44:36 -08:00
Kara Erickson d9ae70c699 test(ivy): normalize template names in canonical spec (#21815)
PR Close #21815
2018-02-08 08:55:40 -08:00
Peter Bacon Darwin a751649c8d fix(core): use appropriate inert document strategy for Firefox & Safari (#17019)
Both Firefox and Safari are vulnerable to XSS if we use an inert document
created via `document.implementation.createHTMLDocument()`.

Now we check for those vulnerabilities and then use a DOMParser or XHR
strategy if needed.

Further the platform-server has its own library for parsing HTML, so we
sniff for that (by checking whether DOMParser exists) and fall back to
the standard strategy.

Thanks to @cure53 for the heads up on this issue.

PR Close #17019
2018-02-08 08:55:15 -08:00
Victor Berchet 3f5a3d6ea1 refactor(ivy): add internal isProceduralRenderer() (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
Victor Berchet 10a014d89e refactor(ivy): prefix viewStart & viewEnd with embedded (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
Victor Berchet 8feb8e5408 refactor(ivy): use long instruction format in tests (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
Victor Berchet 16dada28f5 docs(ivy): Simplify & dedup API docs for canInsertNativeNode (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
Pete Bacon Darwin 67cf7128ae docs(aio): remove ngATL banner from homepage (#22060)
Closes #22029

PR Close #22060
2018-02-07 16:10:17 -08:00
Alex Eagle 16e5b866d2 test(ivy): also track the size of the compressed hello world bundle (#22056)
PR Close #22056
2018-02-07 16:10:00 -08:00
Kapunahele Wong 83d43ac850 docs(aio): remove lifecycle hooks img (#21425)
PR Close #21425
2018-02-07 16:09:44 -08:00
Jesus Rodriguez cd25939be9 build(aio): update examples to CLI to 1.6.5 (#21222)
PR Close #21222
2018-02-07 16:09:26 -08:00
Kara Erickson b58c3527e9 test(ivy): add canonical spec for object literals (#22045)
PR Close #22045
2018-02-07 12:10:16 -08:00
Miško Hevery efc67ee5ef fix(ivy): make pipe invocation locality neutral (#22030)
PR Close #22030
2018-02-07 12:09:56 -08:00
Pete Bacon Darwin 7a406a3896 feat(aio): report logger.error calls to Google Analytics (#22011)
We have a number of observables that have `catch` handlers to recover
from errors without causing the stream to close, and breaking the app.
We also have some `try ... catch` blocks for synchronous code for a
similar reason.

In these cases we conventionally then call `logger.error` in the catch
handler. We are interested in these errors so we are going to capture them
by reporting them to Google Analytics via the new `ReportingErrorHandler`.

PR Close #22011
2018-02-07 12:09:38 -08:00
Pete Bacon Darwin 98001a065d feat(aio): report application errors to Google Analytics (#22011)
This is a basic implementation of error logging using the limited
facilities provided by Google Analytics.

Errors within the Angular app itself will be handled by a new
`ReportingErrorHandler` service, which overrides and extends the
built-in `ErrorHandler`.

Further, errors outside the app, which arrive at `window.onerror`
will also be reported to Google Analytics.

Closes #21943

PR Close #22011
2018-02-07 12:09:38 -08:00
Chuck Jazdzewski e442881ead feat(bazel): allow explicit specification of factories (#22003)
The `ng_module` rule now has a factories attribute that
allows explicit specification of which files are expected
to generate factories. This allows avoiding generating
empty factory files (such as `.ngfactory.js`) begin
generated which might cause down-stream tools issues if
they have a limit on the number of files that can be
processed in a single bazel action.

PR Close #22003
2018-02-07 12:09:21 -08:00
Chuck Jazdzewski b37cee36f9 fix(language-service): correct instructions to install the language service (#22000)
Fixes: #21956

PR Close #22000
2018-02-07 12:09:00 -08:00
Emilio e56de1025a fix(core): ensure initial value of QueryList length (#21980) (#21982)
Set initial value of `length` to `0`.

Fixes regression introduced by https://github.com/angular/angular/commit/e54474215629aa6a0e0497fe61bfc896cea532c9#diff-a85dbe0991a7577ea24b49374e9ae90b where the `length` property ceased to have initial value.

Closes #21980

PR Close #21982
2018-02-07 12:08:44 -08:00
Olivier Combe 64ae6d206e test(common): disable deprecated date pipe tests on chrome mobile (#21933)
Closes #21907
PR Close #21933
2018-02-07 12:07:31 -08:00
Oussama Ben Brahim 54a14312d1 test(forms): update test name with correct wording (#21833)
Use the term primitive value instead of standalone

Fixes #21831

PR Close #21833
2018-02-07 12:07:14 -08:00
George Kalpakas 7e95802cc1 fix(aio): ignore .header-link when selecting the heading text (#21695)
Implemented @maxkorz's
[suggestion](https://github.com/angular/angular/issues/21515#issuecomment-357453634).

Fixes #21515

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas e3e7044d06 fix(aio): prevent heading misplacement while styles load (#21695)
During the initial load of the page (probably until the icon styles are
loaded and/or applied), the `.header-link` element is wider, pushing the
heading text slightly to the right (for a brief moment).

This commit prevents this slight shift by explicitly setting the width
for the `.header-link` element.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas eb3bfc25be fix(aio): ensure header-links are visible at <600px (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 94d769de71 refactor(aio): simplify .header-link styles (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 66191e8a37 fix(aio): reduce flicker and reflows for initial rendering (#21695)
For the initial rendering, where there is no transition from a previous
visual state to a new one, animations make little sense. The page should
load with as few reflows as possible.
Similarly, while we typically want to defer updating the SideNav state
(e.g. opened/closed) until the "leaving" document is animated out of the
page, on the initial rendering (where there is no "leaving" document)
this leads to the SideNav flashing (from closed to open).

These worked as expected before, but several parts (mostly related to
documents with a SideNav) have been accidentally broken in recent
commits (e.g. when upgraded to latest material, or enabled animations
for DocViewer transitions, etc.).

This commit restores the previous behavior by ensuring that (on the
initial rendering) the SideNav state is updated as soon as possible and
that there will be no animations when:

1. The hamburger button appears.
2. The SideNav is opened.
3. The main section's width is adjusted to make room for the SideNav.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas bec188506c refactor(aio): preserve HttpClient asynchronicity in tests (#21695)
Previously, the mocked `HttpClient` was synchronous in tests (despite
the actual `HttpClient` being asynchronous). Although we use observables
(which generally make the implementation sync/async-agnostic), the fact
that we have no control over when Angular updates/checks views and calls
lifecycle hooks resulted in different behavior (and errors) in tests
(with sync `HttpClient`) vs actual app (with async `HttpClient`).

This commit ensures that the behavior (and errors) are consistent
between the tests and the actual app by making the mocked `HttpClient`
asynchronous.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 4f869ff755 fix(aio): remove links from sub-menu toggles (#21695)
Navigating to a document while trying to expand or collapse a sub-menu
is undesirable and confusing. All sub-menu toggles should have no other
effect than expanding/collapsing the corresponding sub-menu.

PR Close #21695
2018-02-07 12:06:45 -08:00
George Kalpakas 8f6047340e docs(animations): fix typo (disbled --> disabled) (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
Philip Pham 9744a1c966 fix(forms): set state before emitting a value from ngModelChange (#21514)
Closes #21513.

PR Close #21514
2018-02-07 12:05:43 -08:00
Kevin Fahy 0bcfae7cac fix(forms): prevent event emission on enable/disable when emitEvent is false (#12366) (#21018)
Previously, the emitEvent flag was only checked when emitting on the current control.
Thus, if  the control was part of a hierarchy, events were emitted on the parent and the childrens.
This fixes the issue by properly passing the emitEvent flag to both parent and childrens.

Fixes #12366

PR Close #21018
2018-02-07 12:05:26 -08:00
Phil McCloghry-Laing 140e7c00d1 fix(forms): make Validators.email support optional controls (#20869)
Bring email validator in line with other validators so that empty values are ignored.

PR Close #20869
2018-02-07 12:05:08 -08:00
harunurhan 941e88ff79 feat(forms): multiple validators for array method (#20766)
Change array method signature so that array of validator and/or async
validatior functions can be passed.

Fixes #20665

PR Close #20766
2018-02-07 12:04:48 -08:00
Pete Bacon Darwin 71ea931df5 build(aio): blacklist unwanted URLs from the generated sitemap.xml (#22061)
Closes #22017

PR Close #22061
2018-02-07 12:02:01 -08:00
jhenderson2099 545fdf10e2 docs(aio): fix TOH inclusion of HeroesService. (#21228)
Change docs where the MessageService is referenced

Fixes #20398

PR Close #21228
2018-02-07 12:01:32 -08:00
Aric Thorn 7e928db204 docs(forms): Custom Validator example selector name incorrect. (#20464)
Added bobby e2e test for template form.

Fixes: #20206

PR Close #20464
2018-02-07 12:01:12 -08:00
Aric Thorn cd4c0eab94 docs(forms): Custom Validator example selector name incorrect. (#20464)
Name of selector in ForbiddenName example is not consistent with Validator class nor Html selector example. Added the selector name 'appForbiddenName' as an alias name for the input of the Validator class, and updated the view accordingly.

Fixes: #20206

PR Close #20464
2018-02-07 12:01:12 -08:00
Alex Rickabaugh 5b06069fd9 docs: add changelog for 6.0.0-beta.3 2018-02-07 11:25:45 -08:00
Alex Rickabaugh d0f3162e84 release: cut the 6.0.0-beta.3 release 2018-02-07 11:23:15 -08:00
Alex Rickabaugh 81537cb161 docs: add changelog for 5.2.4 2018-02-07 11:20:14 -08:00
Alex Eagle 370ab66c4f build(ivy): create hello world rollup (#22004)
This is a customization of the rollup_bundle rule from rules_nodejs
which adds the build-optimizer as a plugin.

Add a functional test with fast round-trip that asserts the minified app
still works.

Publish the min.js artifact on circleCI so we can track its size.

PR Close #22004
2018-02-06 08:25:22 -08:00
Trotyl 2707012181 fix(forms): publish missing types (#19941)
PR Close #19941
2018-02-06 08:02:15 -08:00
Kara Erickson 4d62be69c5 feat(ivy): memoize array literals in render3 (#21973)
PR Close #21973
2018-02-06 08:01:52 -08:00
Marc Laval 7e51e52f55 perf(ivy): improve Uglify configuration in hello world integration test (#21985)
PR Close #21985
2018-02-06 08:01:18 -08:00
Martin Sikora e81606c97a fix(core): fix proper propagation of subscriptions in EventEmitter (#22016)
Closes #21999

PR Close #22016
2018-02-06 07:56:33 -08:00
JiaLi.Passion f791e9f081 fix(core): fix #20582, don't need to wrap zone in location change listener (#20640)
PR Close #20640
2018-02-05 13:06:22 -08:00
A7xSV 3aa7e0228a docs(aio): fix swap value (#20905)
'http.get' has been swapped in for 'of'

PR Close #20905
2018-02-05 13:05:58 -08:00
Judy Bogart 9d3326caa7 docs: clarify npm/yarn commands, add blank lines to mix md/html in table (#21606)
PR Close #21606
2018-02-05 13:02:14 -08:00
Judy Bogart 1940b18124 docs: update browser support (#21606)
PR Close #21606
2018-02-05 13:02:13 -08:00
Victor Berchet 0846784b98 fix(ivy): improve bindV perf and memory usage (#21881)
- Fix the case when first dynamic values are NO_CHANGE
- Do not store the static texts (even indexes) as bindings,
- Do not diff static texts (they do not change),
- Do not stringify static texts,
- Remove superfluous values walking.

PR Close #21881
2018-02-05 13:01:37 -08:00
Victor Berchet 0d10b9002e refactor(ivy): simplify bind code (#21881)
PR Close #21881
2018-02-05 13:01:37 -08:00
Miško Hevery 0c9ec37e26 ci: mark PRs with rejection as not green (#21922)
PR Close #21922
2018-02-05 13:01:11 -08:00
Pete Bacon Darwin 9a0700f5bd build(aio): add API static members to search index (#21988)
Previously searching for `compose` did not include `Validators`
in the search results because we were not including all the
`static` members of API docs in the index.

PR Close #21988
2018-02-05 13:00:47 -08:00
Olivier Combe ae7bc2238d ci: add config for g3 status (#21996)
Ref #21642
PR Close #21996
2018-02-05 12:59:59 -08:00
Victor Berchet 5df626bbe1 refactor(ivy): misc refactoring (#22001)
PR Close #22001
2018-02-05 12:59:34 -08:00
Pete Bacon Darwin 5a624fa1be feat(aio): dynamically, pre-emptively, add noindex (#21992)
These tags are removed when the doc is ready and valid, but this will
allow us to block indexing in the case that the Angular app fails to
bootstrap or load the document for some non-404 reason.

This should get around the problem with hardcoded tags. See
https://github.com/angular/angular/commit/c3fb820473d64036ef0dd3d4c004cc7fbc67be75

Closes #21941

PR Close #21992
2018-02-05 12:58:27 -08:00
JiaLi.Passion 3a86940ca5 fix(core): should check Zone existance when scheduleMicroTask (#20656)
PR Close #20656
2018-02-02 07:53:55 -08:00
Jannis Lehmann 7b120b5f73 docs: consistency fix in describing a custom tag (#21747)
PR Close #21747
2018-02-02 07:53:18 -08:00
Pete Bacon Darwin de25d1886e docs(aio): update docs changelog with links to ts-to-js guide (#21763)
PR Close #21763
2018-02-02 07:52:30 -08:00
Pete Bacon Darwin d77444b88a fix(aio): update Firebase redirects and SW routes (#21763)
Closes #21377

PR Close #21763
2018-02-02 07:52:30 -08:00
Pete Bacon Darwin 240aed29e0 build(aio): test Service Worker "routing" configuration (#21763)
PR Close #21763
2018-02-02 07:52:30 -08:00
Pete Bacon Darwin bf29936af9 build(aio): test Firebase hosting redirection configuration (#21763)
PR Close #21763
2018-02-02 07:52:30 -08:00
Pete Bacon Darwin 339ca83f9d build(aio): move test config and e2e tests into subfolders (#21763)
This is in preparation of putting firebase and service worker
deployment tests into the project.

PR Close #21763
2018-02-02 07:52:30 -08:00
JiaLi.Passion 447783e575 docs: add docs for IE (#21824)
PR Close #21824
2018-02-02 07:51:46 -08:00
Misko Hevery 743d8bc845 feat(ivy): add canonical example of a pipe. (#21834)
PR Close #21834
2018-02-02 07:51:23 -08:00
Chuck Jazdzewski f816666ede fix(ivy): generate lifecycle pattern (#21865)
Implement the lifecycle pattern defined in #21793

PR Close #21865
2018-02-02 07:50:31 -08:00
musicq d3c2aa5f95 docs: add missing underline (#21892)
PR Close #21892
2018-02-02 07:49:32 -08:00
Chuck Jazdzewski 3cc1d76ee7 fix(ivy): generate correct interpolations (#21946)
Ivy compile would generate the an incorrect interpolation if there
were more than 8 interpolations in a text block.

Fixes: #21927

PR Close #21946
2018-02-02 07:49:13 -08:00
Pete Bacon Darwin 124283982b build(aio): move zip and live-example generation to yarn predocs task (#21970)
This will prevent the confusing errors for first time users who
try to generate the docs with `yarn docs` and are told there are
dangling links.

Closes #21944

PR Close #21970
2018-02-02 07:48:42 -08:00
Chuck Jazdzewski 65cf1add97 fix(ivy): remove unnecessary parameter of NgOnChangesFeature (#21879)
PR Close #21879
2018-02-01 08:33:36 -08:00
Alex Rickabaugh 8b14488827 fix(common): don't convert null to a string when flushing a mock request (#21417)
A bug in TestRequest caused null response bodies to be stringified. This
change causes null to be treated faithfully.

Fixes #20744

PR Close #21417
2018-02-01 08:32:43 -08:00
yerkebulan f9fa157a09 docs(aio): add missing closing <code-examle> tag (#21771)
PR Close #21771
2018-02-01 08:31:20 -08:00
Chuck Jazdzewski eb8ddd2983 feat(compiler-cli): reflect static methods added to classes in metadata (#21926)
PR Close #21926
2018-02-01 08:30:58 -08:00
Pawel Kozlowski 1aa2947f70 feat(ivy): add support for attributes on ng-content nodes (#21935)
By adding attributes on the <ng-content> element template authors
can decide how content should be re-projected (or, in other words:
which selectors should match re-projected content).

PR Close #21935
2018-02-01 08:30:26 -08:00
Jason Aden 61abba4bed docs: add changelog for 6.6.0-beta.2 2018-01-31 13:15:40 -08:00
Jason Aden 5da72cc465 release: cut the 6.0.0-beta.2 release 2018-01-31 12:44:04 -08:00
Kara Erickson 3db75b3f64 test(ivy): temp disable payload limit tests (#21940)
PR Close #21940
2018-01-31 11:50:18 -08:00
Alex Rickabaugh db3e65fb17 docs(service-worker): describe safety-worker.js in SW guide (#21921)
PR Close #21921
2018-01-31 10:25:14 -08:00
Ward Bell 7a20691f13 docs: add http guide sample and adjust text (#21326)
PR Close #21326
2018-01-31 10:24:43 -08:00
Chuck Jazdzewski ccd0298ec9 ci(ivy): enable size tracking of a minimal cli render3 application (#21792)
PR Close #21792
2018-01-31 10:21:33 -08:00
Enzo Volkmann eeab433c8d docs: Fix platform-detection example for Universal (#21796)
PR Close #21796
2018-01-31 10:21:04 -08:00
Pete Bacon Darwin c83c4168ca build(aio): upgrade to dgeni-packages 0.24.0 (#21802)
This has two benefits:

* it prepares the way for the API docs update, which need parameter docs
* it doesn't incorrectly report dangling links for non-latin anchors

Closes #21306

PR Close #21802
2018-01-31 10:20:37 -08:00
Chuck Jazdzewski 06d6c76192 fix(ivy): compiler should emit new refresh pattern (#21862)
Change compiler to reflect changes made in #21650

PR Close #21862
2018-01-31 10:19:57 -08:00
Kara Erickson 9dca5f2743 refactor(ivy): save check methods separately (#21795) (#21905)
PR Close #21795

PR Close #21905
2018-01-31 10:19:34 -08:00
Kara Erickson 81306c1f61 feat(ivy): add support for content query (#21912)
PR Close #21912
2018-01-31 10:19:15 -08:00
Kara Erickson 0365592119 test(ivy): add canonical view query example (#21912)
PR Close #21912
2018-01-31 10:19:15 -08:00
Pawel Kozlowski 407b5cf408 refactor(ivy): re-introduce QueryPredicate with more focused role (#21857) (#21857)
PR Close #21857

PR Close #21857
2018-01-30 11:52:35 -08:00
Pawel Kozlowski 4c1743cce3 refactor(ivy): rename QueryPredicate to LQuery (#21857)
PR Close #21857
2018-01-30 11:52:35 -08:00
Pawel Kozlowski 7305e8b45e refactor(ivy): rename LQuery to LQueries and associated renames (#21857)
PR Close #21857
2018-01-30 11:52:32 -08:00
Pawel Kozlowski 285dd6be34 feat(ivy): observable QueryList (#21859)
PR Close #21859
2018-01-30 11:43:38 -08:00
Alex Rickabaugh b10540a0b5 feat(service-worker): add helper script which will uninstall SW (#21863)
Service Workers can be tricky to work with in production, and often
it becomes necessary to deactivate an existing SW. This is trickier
than one might imagine - as long as clients on the old SW may exist
it is important to serve some script at the old SW URL. This commit
adds safety-worker.js to the published NPM package, which is useful
for that purpose. On install the SW unregisters itself which safely
and gradually allows older clients to update.

PR Close #21863
2018-01-30 11:43:03 -08:00
Olivier Combe b62739a989 fix(common): generate closure-locale data file with exported plural functions (#21873)
Fixes #21870
PR Close #21873
2018-01-30 11:42:31 -08:00
Olivier Combe bb577c624b ci: unblock master by ignoring date pipe tests while we fix it (#21906)
PR Close #21906
2018-01-30 11:33:46 -08:00
Jason Aden 02483a01ad Revert: "refactor(ivy): save check methods separately"
This reverts commit 278889c7b420e530834c457f82a54e33a846a828.
2018-01-29 21:42:37 -08:00
Alex Rickabaugh 120bdeecdc fix(common): allow HttpInterceptors to inject HttpClient (#19809)
Previously, an interceptor attempting to inject HttpClient directly
would receive a circular dependency error, as HttpClient was
constructed via a factory which injected the interceptor instances.
Users want to inject HttpClient into interceptors to make supporting
requests (ex: to retrieve an authentication token). Currently this is
only possible by injecting the Injector and using it to resolve
HttpClient at request time.

Either HttpClient or the user has to deal specially with the circular
dependency. This change moves that responsibility into HttpClient
itself. By utilizing a new class HttpInterceptingHandler which lazily
loads the set of interceptors at request time, it's possible to inject
HttpClient directly into interceptors as construction of HttpClient no
longer requires the interceptor chain to be constructed.

Fixes #18224.

PR Close #19809
2018-01-29 16:12:32 -08:00
Kara Erickson 8dff9d84ed refactor(ivy): save check methods separately (#21795)
PR Close #21795
2018-01-29 16:12:02 -08:00
Kara Erickson b4cd27979b fix(forms): inserting and removing controls should work in re-bound form arrays (#21822)
Closes #21501

PR Close #21822
2018-01-29 16:11:41 -08:00
Victor Berchet 11b12670b2 docs: document debugging a Node test in VSCode/Bazel (#21868)
PR Close #21868
2018-01-29 16:11:09 -08:00
Marc Laval dd48df105b perf(ivy): use buildOptimizer in hello_world__render3__rollup integration test (#21744)
PR Close #21744
2018-01-29 11:35:51 -08:00
Chuck Jazdzewski 18174e5564 feat(ivy): support ng-content projection in the ivy compiler (#21764)
PR Close #21764
2018-01-29 11:35:32 -08:00
Bernd Hacker 72265f796f fix(aio): missing plural s in preserveWhiteSpaces example (#21854)
PR Close #21854
2018-01-29 11:35:13 -08:00
Eli Sadoff 6e8bc310f0 docs: change ”it's" to "its" as needed in several docs. (#21867)
Most of them are in content but one is in common and needs special approval.

PR Close #21867
2018-01-29 11:34:46 -08:00
vsavkin c40ae7f7cf feat(router): add navigationSource and restoredState to NavigationStart event (#21728)
Currently, NavigationStart there is no way to know if an navigation was triggered imperatively or via the location change. These two use cases should be handled differently for a variety of use cases (e.g., scroll position restoration). This PR adds a navigation source field and restored navigation id (passed to navigations triggered by a URL change).

PR Close #21728
2018-01-29 10:22:59 -08:00
Misko Hevery 5bd93b1f0f build: fix broken build (#21835)
PR Close #21835
2018-01-27 14:19:29 -08:00
Alex Eagle a1cc02f0bd build: update to latest bazel rules (#21821)
PR Close #21821
2018-01-27 10:55:44 -08:00
Chuck Jazdzewski 5778bb820a fix(ivy): fix issues found producing "Hello, World" example (#21790)
PR Close #21790
2018-01-27 10:50:13 -08:00
Chuck Jazdzewski 87754ad5ec fix(ivy): emit type type fields (#21789)
PR Close #21789
2018-01-27 10:49:54 -08:00
Chuck Jazdzewski d364117aa8 fix(ivy): correct errors in template variable specification (#21759)
PR Close #21759
2018-01-27 10:49:30 -08:00
Pawel Kozlowski 6245637e81 fix(ivy): correct query read logic after merges (#21749)
PR Close #21749
2018-01-27 10:49:09 -08:00
David-Emmanuel Divernois ab69f12e2c refactor(ivy): code review changes (#21638)
PR Close #21638
2018-01-27 10:48:39 -08:00
David-Emmanuel Divernois 1278cca883 perf(ivy): removes generation of comments (#21638)
PR Close #21638
2018-01-27 10:48:39 -08:00
Jason Aden ede9cb7c2f Revert: "feat(router): add navigationSource and restoredState to NavigationStart event (#21728)"
This reverts commit 3b7bab7d22. Will be re-merged after fixing integration of minor breaking change.
2018-01-26 16:24:56 -08:00
Igor Minar c3fb820473 fix(aio): don't set noindex metatag in the static index.html (#21816)
This seems to be causing crawling issues for google.

Ref #21665

PR Close #21816
2018-01-26 16:08:30 -08:00
Victor Berchet 2af19c96f2 fix(core): fix retrieving the binding name when an expression changes (#21814)
fixes #21735
fixes #21788

PR Close #21814
2018-01-26 15:34:47 -08:00
Kara Erickson dcca799dbb fix(ivy): call onChanges before onInit (#21793)
PR Close #21793
2018-01-26 14:57:03 -08:00
Kara Erickson d3d3f7191a test(ivy): add canonical lifecycle example (#21793)
PR Close #21793
2018-01-26 14:57:03 -08:00
Miško Hevery e0b31dbfef build: merge-pr now checks that PR status is green before proceeding (#21810)
Optionally one can use `--force` to override and merge no non-green PR.

PR Close #21810
2018-01-26 14:50:41 -08:00
George Kalpakas 8df56fe93a build(aio): prevent Windows error on serve-and-sync (#21806)
Running `yarn start` (which watches the `src/` directory) and
`yarn docs-watch` (which cleans up files in `src/generated/api/`) often
results in `ENOTEMPTY` errors.

This commit solves it by ensuring that `yarn docs` has been completed
before running `yarn start`.

PR Close #21806
2018-01-26 14:50:15 -08:00
Chuck Jazdzewski 676d9c2c4b fix(language-service): ensure correct paths are passed to TypeScript (#21812)
The 2.6 version of TypeScript's `resolveModuleName`  started to
require paths passed to be separated by '/' instead of being
able to handle '\'.

`ngc` and `ng` already do this transformation.

Fixes: #21811

PR Close #21812
2018-01-26 14:49:23 -08:00
Chuck Jazdzewski 2b68e8d98a fix(language-service): spell diagnostics correctly (#21812)
PR Close #21812
2018-01-26 14:49:23 -08:00
Patrick Shields d964491f2a fix(router): remove @internal tag on ParamInheritanceType (#21773)
This is a more defensive approach to ensure that references to
ParamInheritanceType from the published declarations do not cause
compilation errors when compiling Angular from the published packages.

Fixes #21456

PR Close #21773
2018-01-26 10:28:33 -08:00
Judy Bogart e6080527c6 docs: add notes on email used for CLA (#21754)
Closes #20034

PR Close #21754
2018-01-26 10:28:18 -08:00
vsavkin 3b7bab7d22 feat(router): add navigationSource and restoredState to NavigationStart event (#21728)
Currently, NavigationStart there is no way to know if an navigation was triggered imperatively or via the location change. These two use cases should be handled differently for a variety of use cases (e.g., scroll position restoration). This PR adds a navigation source field and restored navigation id (passed to navigations triggered by a URL change).

PR Close #21728
2018-01-26 10:25:32 -08:00
George Kalpakas 108fa15792 fix(aio): close SideNav on non-sidenav doc on wide screen (#21538)
Partly addresses #21520.

PR Close #21538
2018-01-26 10:25:15 -08:00
George Kalpakas 1b2271a3b1 fix(aio): fix SideNav height on narrow screens (#21538)
Since we specify `bottom: 0`, specifying the height is unnecessary and
leads to wrong height (unless updated) on narrow screens where the
topbar height is decreased.

Partly addresses #21520.

PR Close #21538
2018-01-26 10:25:15 -08:00
Pawel Kozlowski f9381e42de feat(ivy): implement QueryList array-related methods (#21778)
PR Close #21778
2018-01-25 22:19:43 -08:00
Chuck Jazdzewski bbb8f386f1 feat(ivy): implement template variables (#21760)
PR Close #21760
2018-01-25 22:19:20 -08:00
Alex Eagle 08aa54e1d9 ci: Add back the CLI integration test with pinning (#21555)
The CLI app is now checked in, rather than generated dynamically with
`ng new`. This loses some assertion power, but gains hermeticity.
It also checks in lock files for all integration tests, avoiding
floating version numbers.

We'll need another place to integration test between changes in
the various repositories - but the angular/angular PR-blocking status
is not the right place to do this.

PR Close #21555
2018-01-25 22:18:55 -08:00
Filipe Silva fac4d8d42a build(common): specify explicit locales dir in package.json (#21016)
PR Close #21016
2018-01-25 22:18:35 -08:00
Oussama Ben Brahim 170885c51b fix(forms): allow FormBuilder to create controls with any formState type (#20917)
Align formState type in FormBuilder#control with FormControl#constructor

Fixes #20368

PR Close #20917
2018-01-25 22:17:43 -08:00
Miško Hevery 5713faa667 build: autosquashes SHAs as part of merge-pr script (#21791)
To support `git checkin --fixup` and `git checkin —squash`
we need to make sure that `merge-pr` squashes the sepecial
commits before they are merged.

For more details see:
https://robots.thoughtbot.com/autosquashing-git-commits

PR Close #21791
2018-01-25 22:12:11 -08:00
Pete Bacon Darwin 23596b3f30 docs(aio): fix missing stylesheet in component-styles example (#21772)
The code in the example was referring to `hero-app.component.css` but this did
not exist.

PR Close #21772
2018-01-25 13:38:12 -08:00
Pete Bacon Darwin f8fa20d71a docs(aio): fix paths to imported CSS stylesheets (#21772)
The AOT compiler needs relative paths so that it can find
the imported stylesheets.

PR Close #21772
2018-01-25 13:38:12 -08:00
Pete Bacon Darwin d6d8fe829a build(aio): upgrade CLI version to cope with new Angular 6.0.0-beta.1 release (#21772)
Before version 1.6 of Angular CLI there was a check that prevented use of Angular
compiler CLI with major version 6.

PR Close #21772
2018-01-25 13:38:12 -08:00
Pawel Kozlowski 5269ce287e feat(ivy): support deep queries through view boundaries (#21700)
PR Close #21700
2018-01-24 22:18:38 -08:00
Kara Erickson 3e03dbe576 refactor(ivy): flatten hooks and collapse LView hook properties (#21650)
PR Close #21650
2018-01-24 22:14:33 -08:00
Kara Erickson 33b338120c refactor(ivy): move onDestroys out of cleanup (#21650)
PR Close #21650
2018-01-24 22:14:33 -08:00
Kara Erickson 811679a583 refactor(ivy): remove unnecessary Comp.r function (#21650)
PR Close #21650
2018-01-24 22:14:33 -08:00
Kara Erickson 2c33d17609 refactor(ivy): move content and view hooks into TView (#21650)
PR Close #21650
2018-01-24 22:14:32 -08:00
Kara Erickson 9c99e6a838 refactor(ivy): move init hooks into TView (#21650)
PR Close #21650
2018-01-24 22:14:32 -08:00
Kara Erickson 98174758ad refactor(ivy): add type and hooks to directive def (#21650)
PR Close #21650
2018-01-24 22:14:32 -08:00
Kara Erickson 97b928053d fix(ivy): correct onDestroy order for projected components (#21650)
PR Close #21650
2018-01-24 22:14:32 -08:00
Kara Erickson 1fe55e252c feat(ivy): add afterContentInit and afterContentChecked to render3 (#21650)
PR Close #21650
2018-01-24 22:14:32 -08:00
Kara Erickson 53ed4b4648 test(ivy): add missing lifecycle tests for projected components (#21650)
PR Close #21650
2018-01-24 22:14:32 -08:00
Misko Hevery d08785d2e1 docs: add changelog for 6.0.0-beta.1 2018-01-24 22:01:19 -08:00
Misko Hevery c24533eb51 release: cut the 6.0.0-beta.1 release 2018-01-24 21:58:09 -08:00
Misko Hevery 4b1aadee94 docs: add changelog for 5.2.2 2018-01-24 21:53:10 -08:00
Miško Hevery e4b11b2de6 Revert "fix(aio): update redirects to fix unwanted 404s (#21712)"
This reverts commit f9a6a94be4.
2018-01-24 15:53:57 -08:00
Miško Hevery 92ddd46bcc Revert "docs(aio): update docs changelog with links to ts-to-js guide (#21712)"
This reverts commit 43d1b9864f.
2018-01-24 15:50:41 -08:00
Miško Hevery bf5f50d9ee Revert "build(aio): add tests for Firebase redirection (#21712)"
This reverts commit 01cef016ee.
2018-01-24 15:50:00 -08:00
Miško Hevery 753622414a Revert "fix(aio): add missing forward slash to redirect (#21712)"
This reverts commit f11b2fb00b.
2018-01-24 15:49:42 -08:00
Miško Hevery c6ecb638a0 Revert "fix(aio): add missing leading slash on firebase redirect (#21761)"
This reverts commit 12f801e3dc.
2018-01-24 15:49:27 -08:00
Zhicheng Wang 5f681f9745 fix(compiler): Don't strip /*# sourceURL ... */ (#16088)
Currently, `shimCssText` only keep `/*# sourceMappingUrl ... */` comments and strip `/*# sourceURL ... */` comments. So, Chrome can't find the source maps for component style(that's created in new `style` tags)

PR Close #16088
2018-01-24 12:35:30 -08:00
Miško Hevery c7c5214029 build: merge PR to all branches per target: label (#21739)
PR Close #21739
2018-01-24 12:35:11 -08:00
Pete Bacon Darwin 12f801e3dc fix(aio): add missing leading slash on firebase redirect (#21761)
PR Close #21761
2018-01-24 12:34:33 -08:00
Patrick Shields 925e654a29 fix(router): don't use ParamsInheritanceStrategy in declarations (#21574)
ParamsInheritanceStrategy is internal, so any references to it from the
published .d.ts files will fail.

Fixes #21456.

PR Close #21574
2018-01-23 21:34:37 -08:00
George Kalpakas d0e086a5fe fix(aio): remove remaining plnkr references (#20165)
PR Close #20165
2018-01-23 21:30:27 -08:00
George Kalpakas 15c2a93f14 build(aio): check for obsolete plnkr.json and missing main files (#20165)
Also, remove `plnkr.json` for `service-worker-getting-started` guide,
since it is not used and ServiceWorker cannot work correctly in
plnkr/stackblitz anyway (e.g. no build step to re-compute hashes).
A zipper might be useful and can be added in a subsequent PR, but it is
currently broken (e.g. no dependency on `@angular/service-worker`).

PR Close #20165
2018-01-23 21:30:27 -08:00
Ward Bell 8522546a8a build(aio): upgrade sample package.json files to jasmine@~2.8.0 (#20165)
- Update tooling to support revised testing guide (PR #20697).
- Require jasmine upgrade for examples that use marble testing.
- Copy `cli/package.json` to `testing/` and add `jasmine-marbles`.
- Resolve merge conflicts created by `NgModules` guides.

PR Close #20165
2018-01-23 21:30:27 -08:00
Jesus Rodriguez 1a75934cc0 build(aio): migrate plunker to stackblitz (#20165)
PR Close #20165
2018-01-23 21:30:27 -08:00
Miško Hevery 1e9cd95f5c test(ivy): content projection canonical example (#21674)
PR Close #21674
2018-01-23 16:42:07 -08:00
Victor Berchet acf381bcb7 docs: update ICU select messages to use male/female (#21713)
fixes #21694

PR Close #21713
2018-01-23 16:32:23 -08:00
Victor Berchet cb5090cdc8 fix(compiler): fix ICU select messages to use male/female/other (#21713)
related to #21694

PR Close #21713
2018-01-23 16:32:23 -08:00
ghetolay 4b70bcd200 test(forms): Better description and coverage for #19256 (#21652)
fixes #21575

PR Close #21652
2018-01-23 16:31:45 -08:00
Chuck Jazdzewski 8baff1858b fix(ivy): add names to function expressions (#21714)
PR Close #21714
2018-01-23 13:33:48 -08:00
Pete Bacon Darwin f11b2fb00b fix(aio): add missing forward slash to redirect (#21712)
PR Close #21712
2018-01-23 13:33:36 -08:00
Pete Bacon Darwin 01cef016ee build(aio): add tests for Firebase redirection (#21712)
PR Close #21712
2018-01-23 13:33:36 -08:00
Pete Bacon Darwin 43d1b9864f docs(aio): update docs changelog with links to ts-to-js guide (#21712)
PR Close #21712
2018-01-23 13:33:36 -08:00
Pete Bacon Darwin f9a6a94be4 fix(aio): update redirects to fix unwanted 404s (#21712)
Closes #21377

PR Close #21712
2018-01-23 13:33:36 -08:00
Chuck Jazdzewski dd8679037e fix(compiler-cli): do not fold errors past calls in the collector (#21708)
Folding errors passed calls prevented the static reflector from
begin able to ignore errors in annotations it doesn't know as
the call to the unknown annotation was elided from the metadata.

Fixes: #21273

PR Close #21708
2018-01-23 13:33:25 -08:00
JiaLi.Passion e82812b9a9 docs: fix #19989, add zone flags(blacklist/module) in guide (#21701)
PR Close #21701
2018-01-23 13:33:10 -08:00
Chuck Jazdzewski c65634215b feat(ivy): update specification to include template variables (#21677)
PR Close #21677
2018-01-23 13:32:57 -08:00
Alex Rickabaugh 0ad02de47e feat(ivy): support for the ngForOf directive, with tests (#21430)
Implement NgOnChangesFeature, ViewContainerRef, TemplateRef,
and the renderEmbeddedTemplate instruction, and wire together the
pieces required for the ngForOf directive to work.

PR Close #21430
2018-01-23 12:54:39 -08:00
Alex Rickabaugh 6472661ae8 refactor(ivy): avoid circular dep with query/di/instructions (#21430)
To prepare for pending ngForOf work, the dep from instructions -> query
should be broken. This will enable a dep from di -> instructions while
avoiding a di -> instructions -> query -> di cycle.

Analyzing this cycle also uncovered another problem: the implementation
of query() breaks tree-shaking through a hard dependency on DI concepts
of TemplateRef, ElementRef, ViewContainerRef. This is fundamentally due
to how query() can query for those values without any configuration.

Instead, this fix introduces the concept by employing the strategy
pattern, and redefining QueryReadType to pass a function which will
return one of the above values. This strategy is then used for 'read'
instead of an enum in cases where special values should be read from
the DI system.

PR Close #21430
2018-01-23 12:54:39 -08:00
Alex Rickabaugh c5586b7dfa fix(ivy): assertLessThan asserts the wrong thing (#21430)
assertLessThan() actually does the opposite of what it advertises.
It's only through luck that existing asserts have not failed
before. This changes assertLessThan to actually assert that the
value is less than something.

PR Close #21430
2018-01-23 12:54:39 -08:00
Alex Eagle 95fbb7d675 build: Update to latest rules_typescript. (#21675)
Fixes #21481

PR Close #21675
2018-01-22 15:32:59 -08:00
Pete Bacon Darwin a64af40c0b build(aio): generate sitemap from the generated pages (#21689)
Closes #21684

PR Close #21689
2018-01-22 12:55:15 -08:00
Max Ivanov 2015ccd1f5 docs(aio): added a link to Angular-RU (#21687)
Angular-RU Community on GitHub is a single point for all resources, chats, podcasts and meetups for Angular in Russia

PR Close #21687
2018-01-22 12:55:03 -08:00
Victor Berchet 7d49443060 fix(common): A null value should remove the style on IE (#21679)
fixes #21064

PR Close #21679
2018-01-22 12:54:49 -08:00
Chuck Jazdzewski 86d9612230 feat(ivy): update compiler to specification (#21657)
PR Close #21657
2018-01-20 09:28:44 -08:00
Denny Brown 8c51c276c7 docs: fix stray div and reformat paragraph (#21676)
PR Close #21676
2018-01-19 20:40:13 -08:00
Olivier Combe 6c51665c82 ci: add github bot config to triage issues (#21672)
Fixes #21635
PR Close #21672
2018-01-19 20:39:58 -08:00
Yuan Gao d39d1ce412 ci(aio): do not limit size of gzip7 and gzip 9 (#21601)
PR Close #21601
2018-01-19 20:39:42 -08:00
Pete Bacon Darwin 88045a5050 feat(aio): update metatags to control search engine crawling (#21665)
The `<meta name="robots" content="noindex">` tag is used
to indicate to search engine crawlers that they should not index
the current page. This is set dynamically by the the document
viewer component to ensure that 404 and other erroring pages
are not added to the search index.

This relies upon the idea that the crawling bot will run the JS
and wait to see if this meta tag has been added or not.

Since we believe that the `googebot` will do this, we also
pre-emptively add a hard-coded noindex tag specifically for
this bot, so that if anything else fails in bootstrapping the app,
the failed page will not be added to the index.

Closes #21317

PR Close #21665
2018-01-19 20:30:02 -08:00
Marc Laval 0b38a039d0 perf(ivy): add a proper creationOnly scenario to the tree benchmark (#21503)
PR Close #21503
2018-01-19 15:24:06 -08:00
Marc Laval a0dc0a2f46 test(ivy): add render3 integration tests (#21557)
PR Close #21557
2018-01-19 15:23:52 -08:00
Marc Laval 6397062e22 perf(ivy): set ngDevMode to false in render3 benchmarks (#21502)
PR Close #21502
2018-01-19 15:23:33 -08:00
Kara Erickson e18f1de003 refactor(ivy): remove unnecessary D instruction (#21484)
PR Close #21484
2018-01-19 15:23:17 -08:00
Chuck Jazdzewski 21e37e47c6 fix(ivy): update the compiler specification (#21656)
Also make a minor fix for directive definitions

PR Close #21656
2018-01-19 13:23:54 -08:00
George Kalpakas 8e8924ac7c ci: use sudo: false on Travis (#21641)
Related to #21422.

PR Close #21641
2018-01-19 13:23:47 -08:00
George Kalpakas b2e902deb4 fix(aio): fix code highlight in API docs templates (#21630)
Fixes #21108

PR Close #21630
2018-01-19 13:23:41 -08:00
Olivier Combe 97b18b2a5c fix(common): extract plural function from i18n locale data files for TS 2.6 (#21626)
Fixes #21608

PR Close #21626
2018-01-19 13:23:34 -08:00
Olivier Combe 135a2822ea fix(common): don't remove special characters when extracting CLDR data (#21626)
PR Close #21626
2018-01-19 13:23:34 -08:00
Alex Eagle 1104d17252 refactor(bazel): pass around tsconfig as a file, not a path (#21614)
this unlocks the ability to replay ts compilations with different settings

PR Close #21614
2018-01-19 11:53:59 -08:00
Alex Eagle a1492a73ce build: Remove angular_src nested workspace (#21096)
PR Close #21096
2018-01-19 10:17:37 -08:00
Fabian Wiles c8a1a14b87 docs: clarify the use of classes and interfaces in style guide (#20919)
PR Close #20919
2018-01-18 18:22:55 -06:00
Chuck Jazdzewski 64d16dee02 feat(compiler): implement "enableIvy" compiler option (#21427)
The "enableIvy" compiler option is the initial implementation
of the Render3 (or Ivy) code generation. This commit enables
generation generating "Hello, World" (example in the test)
but not much else. It is currenly only useful for internal Ivy
testing as Ivy is in development.

PR Close #21427
2018-01-18 18:22:44 -06:00
Igor Minar ce8b5877e2 ci: add "PR action: cleanup" to the bot's forbiddenLabels list (#21562)
PR Close #21562
2018-01-18 18:22:32 -06:00
George Kalpakas 44a4b1680f docs: improve/simplify example for providers guide (#21589)
PR Close #21589
2018-01-18 18:22:20 -06:00
George Kalpakas 4596b9d0df docs: fix/improve example for singleton-services guide (#21589)
PR Close #21589
2018-01-18 18:22:20 -06:00
George Kalpakas 81e87095b4 docs: several minor NgModule guide fixes/improvements (#21589)
PR Close #21589
2018-01-18 18:22:20 -06:00
George Kalpakas bf248792eb docs: minor fixes (anchor tags, redundant whitespace, consistent code-snippets lang) (#21589)
PR Close #21589
2018-01-18 18:22:20 -06:00
Vani 1ce46b56f8 docs: change titles to sentence case (#21620)
PR Close #21620
2018-01-18 18:22:09 -06:00
George Kalpakas d07760f5e3 build(aio): fix zips testing commands (#21629)
PR Close #21629
2018-01-18 18:21:59 -06:00
Olivier Combe 2b4f3004ac ci: update github bot messages (#21634)
Fixes #21633
PR Close #21634
2018-01-18 18:21:52 -06:00
Chuck Jazdzewski 7f93aad836 fix(compiler-cli): do not lower expressions in non-modules (#21649)
Fixes: #21651

PR Close #21649
2018-01-18 18:21:42 -06:00
Trotyl 879756d44c fix(common): fallback to last defined value for named date and time formats (#21299)
closes #21282

PR Close #21299
2018-01-18 13:39:00 -06:00
Chuck Jazdzewski 2cf9d6d75b fix(aio): add a required comma in firebase.json 2018-01-17 19:32:29 -08:00
Kapunahele Wong af2a843446 docs: edit entry component FAQ (#21487)
PR Close #21487
2018-01-17 17:14:33 -08:00
Kapunahele Wong 02377335ec docs: add server side redirect and fix NgModule FAQ links (#21487)
PR Close #21487
2018-01-17 17:14:33 -08:00
Kapunahele Wong e79e98ab44 docs: fix lazy loading example dir name (#21475)
PR Close #21475
2018-01-17 17:02:41 -08:00
Misko Hevery 52cfe3952a build: add mhevery to bazel approvers (#21314)
PR Close #21314
2018-01-17 17:02:22 -08:00
Olivier Combe f74130c9f7 fix(compiler): add support for marker tags in xliff serializers (#21250)
The Xliff serializer now supports the tags `seg-source` and `mrk`, while the Xliff2 serializer now supports `mrk`.
Fixes #21078
PR Close #21250
2018-01-17 17:02:09 -08:00
harunurhan d3d9aac4e9 feat(core): optional generic type for ElementRef (#20765)
Add optional, backwards compatible generic type to `ElementRef` to
support typed `nativeElement`

Fix #13139

PR Close #20765
2018-01-17 17:01:49 -08:00
Miško Hevery 1ccc3242f1 docs(ivy): add feature principle doc (#21565)
PR Close #21565
2018-01-17 17:01:09 -08:00
Chuck Jazdzewski 47b7898697 Revert "fix(core): fix chained http call (#20924)"
This reverts commit 7e3f9a482a.
2018-01-17 15:28:21 -08:00
Chuck Jazdzewski 0eabd07f3a fix: avoid triggering a cli bug (#21611)
Temporary update to the package.json file until CLI is fixed to
handle 6.0 builds.
2018-01-17 15:22:06 -08:00
Alex Eagle 8503cc1db7 docs: add changelog for 6.0.0-beta.0 2018-01-17 10:10:18 -08:00
Alex Eagle 8b46b3893f release: cut the 6.0.0-beta.0 release 2018-01-17 10:08:53 -08:00
Alex Eagle bb3b93c6e6 docs: add changelog for 5.2.1 2018-01-17 09:40:11 -08:00
Alex Eagle a8fe0cb733 build(bazel): remove spurious file (#21455)
PR Close #21455
2018-01-17 08:24:20 -08:00
Alan Agius 030c68df32 docs(aio): change df-question to app-question (#21438)
closes: #21404
PR Close #21438
2018-01-17 07:20:17 -08:00
Jason Aden 6e8cf63b8c docs(aio): add description and docs links for code samples (#21561)
PR Close #21561
2018-01-17 07:19:13 -08:00
Xander Garbett d3bf54bdeb feat(core): add binding name to content changed error (#20352)
Adding the binding name to the error message recieved by the user gives
extra context on what exactly changed. The tests are also updated to
reflect the new error message.

PR Close #20352
2018-01-17 07:16:40 -08:00
Benjamin Ingberg 7e3f9a482a fix(core): fix chained http call (#20924)
Fixes an issue where chained http calls would prematurely call
testability whenStable callbacks after the first http call.

Fixes #20921

PR Close #20924
2018-01-17 07:13:57 -08:00
Brian Douglas fb4d84d5b8 refactor(core): Refactor IterableDiffers#create method (#21383)
Remove unneeded else clause to show that `new IterableDiffers(factories);` is returned by default.

PR Close #21383
2018-01-16 16:26:32 -08:00
Kara Erickson efe545a878 refactor(ivy): add TView and TContainer (#21463)
PR Close #21463
2018-01-16 10:51:55 -08:00
Victor Berchet 3bcc0e6f76 refactor(core): refactor WrappedValue (#20997)
- Improve `WrappedValue` by adding `unwrap` symetrical to `wrap`.
- remove dead code - `ValueUnwrapper`

The property `wrapped` is an implementation details and should never be accessed
directly - use `unwrap(wrappedValue)`. Will change to protected in Angular 7.

PR Close #20997
2018-01-16 07:12:58 -08:00
ghetolay 54bf179888 feat(forms): handle string with and without line boundary on pattern validator (#19256)
PR Close #19256
2018-01-16 07:11:54 -08:00
Pete Bacon Darwin d6ba9f9fb4 build(aio): move file cleaning to later in the doc gen (#21540)
Previously the generated files were cleaned out before
doc-gen began (via a yarn pre-script). This can cause a
race condition in the CLI server, which prevents the new
generated files from being picked up.

Now we delay the cleaning until the last minute to ensure
that they ar still picked up by the webpack server.

PR Close #21540
2018-01-16 07:10:30 -08:00
Alex Eagle 7c414fc746 ci: disable integration/cli-hello-world test (#21492)
it is non-hermetic and breaks often due to unpinned dependencies.

PR Close #21492
2018-01-12 14:58:00 -08:00
Keen Yee Liau 43e1520260 fix(language-service): Clear caches when program changes (#21337)
This commit fixes a bug whereby the caches are not cleared when the
program changes. This subsequently produces the incorrect error of
'Component ... is not included in a module ...'.

PR Close #19405

PR Close #21337
2018-01-12 14:43:01 -08:00
Chuck Jazdzewski 2d44a2ab0d docs(compiler): document the $any type cast function (#20968)
Closes #20966

PR Close #20968
2018-01-12 14:37:26 -08:00
Pete Bacon Darwin c5c6d84fe6 style(aio): enforce strict TypeScript checks (#21342)
Closes #20646

PR Close #21342
2018-01-12 14:36:43 -08:00
Olivier Combe 246de65140 test(common): make date pipe tests work in more timezones (#21379)
Fixes #21112
PR Close #21379
2018-01-12 13:49:46 -08:00
George Kalpakas 3e3c22f507 fix(aio): correctly handle redirects (#21416)
- Fixes handling of some redirects by the ServiceWorker.
- Fixes redirect for old `NgFor` to new `NgForOf` URL.

Fixes #21318

PR Close #21416
2018-01-12 13:48:49 -08:00
George Kalpakas f6db9521ab ci(aio): run e2e tests in production mode (#21470)
This will enable catching errors introduced by build optimizations that
do not appear in `development` mode.

Fixes #21446

PR Close #21470
2018-01-12 13:45:28 -08:00
Olivier Combe 05208b8513 fix(common): set correct timezone for ISO8601 dates in Safari (#21506)
Fixes #21491
PR Close #21506
2018-01-12 13:20:48 -08:00
Olivier Combe bb62458566 docs: fix release schedule date (#21474)
PR Close #21474
2018-01-12 12:10:08 -08:00
Chuck Jazdzewski cffa0fe734 feat(bazel): allow ng_module rules to control whether type checking is enabled (#21460)
Defaults to true which is different than `ngc` which defaults to false.

PR Close #21460
2018-01-12 12:02:48 -08:00
David-Emmanuel Divernois 9b5a485243 perf(ivy): add missing dom element in render3_function tree benchmark (#21476)
Adds a missing <tree> wrapper element in the render3_function tree
benchmark to make it more consistent with other perf tests.

PR Close #21476
2018-01-12 10:48:05 -08:00
Olivier Combe 63478d7a3c ci: add angular robot config file (#21489)
PR Close #21489
2018-01-12 10:12:11 -08:00
Igor Minar 50ca01e255 ci: update pullapprove rules now that we allow selfapprovals (#21494)
PR Close #21494
2018-01-12 10:11:21 -08:00
Pete Bacon Darwin 4f3149242f feat(aio): implement survey notification link (#21371)
Closes #21094

PR Close #21371
2018-01-12 10:05:13 -08:00
Alex Eagle 6af3672185 fix(ivy): Add workaround for AJD in google3 (#21488)
PR Close #21488
2018-01-11 13:57:55 -08:00
Marc Laval 9b84a325ff fix(benchpress): should still support selenium_webdriver < 3.6.0 (#21477)
PR Close #21477
2018-01-11 10:53:44 -08:00
Kapunahele Wong 00300f66e8 docs(forms): clarify note in reactive forms doc (#21134)
PR Close #21134
2018-01-11 07:03:55 -08:00
Miško Hevery 5eaaac35a8 refactor(ivy): remove type from DirectiveDef (#21374)
This change makes the code cleaner for the user. It does mean
a little bit more work for us since we have to patch the `type` back
into the `DirectiveDef`. However since the patching happens only once
on startup it should not be significant.

PR Close #21374
2018-01-11 07:02:18 -08:00
Miško Hevery 16232f000f refactor(ivy): merged containerStart/containerEnd (#21374)
This separation is no longer needed since directives are now passed into the `container` as an array rather than as child functions of the `containerStart`

PR Close #21374
2018-01-11 07:02:18 -08:00
Misko Hevery 9f43f5f09e test(ivy): add canonical template translation examples (#21374)
This change creates a spec file which contains canonical examples
of how the template compiler will translate templates into expected
output.

PR Close #21374
2018-01-11 07:02:18 -08:00
Misko Hevery a6d41c47a9 refactor(ivy): move directive into elementStart (#21374)
We used to have a separate `directive` instruction for instantiating
directives. However, such an instruction requires that directives
are created in the correct order, which would require that template
compiler would have knowledge of all dependent directives. This
would break template compilation locality principle.

This change only changes the APIs to expected form but does
not change the semantics. The semantics will need to be corrected
in subsequent commits. The semantic change needed is to
resolve the directive instantiation error at runtime based on
injection dependencies.

PR Close #21374
2018-01-11 07:02:17 -08:00
Olivier Combe c0080d76c4 docs: fix release schedule date (#21469)
PR Close #21469
2018-01-11 06:45:29 -08:00
Kara Erickson c6eb9eebcc build: update polyfill size (#21461)
PR Close #21461
2018-01-10 17:06:51 -08:00
Kapunahele Wong 456f57dde8 docs: fix lazy-loading example (#20306)
PR Close #20306
2018-01-10 16:26:46 -08:00
Kapunahele Wong f82013f956 docs: fix yarn.lock (#20306)
PR Close #20306
2018-01-10 16:26:46 -08:00
Kapunahele Wong 53f91189a1 docs(aio): add NgModule docs (#20306)
PR Close #20306
2018-01-10 16:26:46 -08:00
Chuck Jazdzewski e64b1e99c2 fix(compiler): make .ngsummary.json files idempotent (#21448)
Fixes: #21432

PR Close #21448
2018-01-10 16:20:53 -08:00
Kara Erickson 6be9c0466c refactor(core): split up interface files in render3 (#21433)
PR Close #21433
2018-01-10 16:13:44 -08:00
Miško Hevery a33ff2c68f docs(ivy): add Ivy as a recognized label (#21428)
PR Close #21428
2018-01-10 15:31:54 -08:00
Igor Minar 92984ba794 docs: update the release schedule with v6 info (#21435)
Fixes #20649

PR Close #21435
2018-01-10 15:12:09 -08:00
George Kalpakas 137b9c784c fix(aio): preserve static Observable methods (#21351)
PR Close #21351
2018-01-10 14:30:21 -08:00
Igor Minar 85013ad160 build(aio): turn on namedChunks option to make debugging, profiling and tracking easier (#21351)
PR Close #21351
2018-01-10 14:30:21 -08:00
Igor Minar 8be11e49e7 build(aio): upgrade to angular@5.2.0 (#21351)
-rw-r--r--  1 iminar  eng   72498 Jan  9 22:20 dist/0.0d2802b63e9f4f4615cd.chunk.js
-rw-r--r--  1 iminar  eng   14872 Jan  9 22:20 dist/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  9 22:20 dist/inline.cb4b7a639193fc489d3b.bundle.js
-rw-r--r--  1 iminar  eng  454654 Jan  9 22:20 dist/main.b65f32ffc92e075e64e0.bundle.js
-rw-r--r--  1 iminar  eng   40264 Jan  9 22:20 dist/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  9 22:20 dist/worker-basic.min.js

PR Close #21351
2018-01-10 14:30:21 -08:00
Igor Minar a0a73fe030 ci(aio): track sizes of all js files (#21351)
PR Close #21351
2018-01-10 14:30:21 -08:00
Igor Minar 7c9ba2d6f2 build(aio): upgrade to angular@5.2.0-rc.0 (#21351)
Because of c2b3792 one of the test assertions had to be adjusted (more info: https://github.com/angular/angular/pull/21351#issuecomment-356083940).

PR Close #21351
2018-01-10 14:30:21 -08:00
Alex Eagle 2ad92ed254 build(bazel): fix merge conflict between c4f02e2 and ef956a2 (#21453)
PR Close #21453
2018-01-10 12:57:14 -08:00
Chuck Jazdzewski e3e2fc0c3b fix(compiler): cache external reference resolution (#21359)
Cache reference resolution for external references as finding
the declaration of a symbol is expensive and does not change
for a program once created.

This resolves a signficant performance regression in the langauge
service.

PR Close #21359
2018-01-10 12:33:52 -08:00
Olivier Combe 7493b8ae10 build: upgrade yarn to 1.3.2 (#21406)
Fixes #20566
PR Close #21406
2018-01-10 12:32:26 -08:00
Alex Eagle c4f02e21dd build: move repeated tsconfig attributes to a macro (#20964)
This helps ensure we use the same tsconfig.json file for all compilations.
Next steps are to make it the same tsconfig.json file used by the editor

PR Close #20964
2018-01-10 12:30:19 -08:00
Suguru Inatomi f15ca6ce50 fix(aio): support multibyte character in heading (#21414)
PR Close #21414
2018-01-10 12:25:06 -08:00
Alex Eagle 0de219adcd docs(bazel): document an installation issue (#21401)
observed by Igor on a Mac

PR Close #21401
2018-01-10 12:23:39 -08:00
Igor Minar ff6047cb58 build(aio): upgrade to uglify-es@3.3.5 (#21350)
This should fix the size regression spotted via the previous commit.

-rw-r--r--  1 iminar  eng   72498 Jan  8 00:03 dist/0.86a7a08f7866e6cdc36f.chunk.js
-rw-r--r--  1 iminar  eng   14872 Jan  8 00:03 dist/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  8 00:03 dist/inline.7722895d8c844f710bcd.bundle.js
-rw-r--r--  1 iminar  eng  453905 Jan  8 00:03 dist/main.faff0e2da95443f759f5.bundle.js
-rw-r--r--  1 iminar  eng   40264 Jan  8 00:03 dist/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  8 00:03 dist/worker-basic.min.js

PR Close #21350
2018-01-10 12:21:55 -08:00
Igor Minar c6b2f07fd7 build(aio): upgrade to @angular/cli@1.6.3 (#21350)
This change introduces a size regression of 9kb for main.js :-(

I filed an issue for this: https://github.com/angular/angular-cli/issues/9108

-rw-r--r--  1 iminar  eng   72546 Jan  5 19:27 dist.cli-1.6.3/0.86a7a08f7866e6cdc36f.chunk.js
-rw-r--r--  1 iminar  eng   14893 Jan  5 19:27 dist.cli-1.6.3/4.c719ac5645940382cdce.chunk.js
-rw-r--r--  1 iminar  eng    1558 Jan  5 19:27 dist.cli-1.6.3/inline.7722895d8c844f710bcd.bundle.js
-rw-r--r--  1 iminar  eng  466484 Jan  5 19:27 dist.cli-1.6.3/main.faff0e2da95443f759f5.bundle.js
-rw-r--r--  1 iminar  eng   40363 Jan  5 19:27 dist.cli-1.6.3/polyfills.87edf5d695f14a29bf91.bundle.js
-rw-r--r--  1 iminar  eng   54001 Jan  5 19:27 dist.cli-1.6.3/worker-basic.min.js

PR Close #21350
2018-01-10 12:21:55 -08:00
courtneypattison a76ab1f42a docs(router): remove extra brackets (#21294)
PR Close #21294
2018-01-10 12:20:16 -08:00
Alex Rickabaugh 395109817b fix(service-worker): properly handle invalid hashes in all scenarios (#21288)
When the SW fetches URLs listed in a manifest with hashes, it checks
the content hash against the manifest to make sure it has the correct
version of the URL. In the event of a mismatch, the SW is supposed to
consider the manifest invalid, and avoid using it. There are 3 cases
to consider by which this can happen.

Case 1: during the initial SW installation, a manifest is activated
without waiting for every URL to be fully loaded. In the background,
every prefetch URL listed by the manifest is requested and cached.
One such prefetch request could fail the hash test, and cause the
manifest to be treated as invalid. In such a case, the SW should
enter a state of EXISTING_CLIENTS_ONLY, as the latest manifest is
invalid.

This case works today.

Case 2: during the initial SW installation, as in Case 1, a manifest
is activated without waiting for each URL to fully load. However,
it's possible that the application could request a URL with a bad
hash before background initialization tries to load that URL. This
happens if, for example, the application has a broken index.html.

In this case, the SW should enter a state of EXISTING_CLIENTS_ONLY,
and serve the request from the network instead.

What happens today is that the internal error escapes the SW and
is returned as a rejected Promise to respondWith(), causing a
browser-level error that the site cannot be loaded, breaking the
site.

This change allows the SW to detect the error and enter the correct
state, falling back on the network if needed.

Case 3: during checkForUpdate(), the SW will try to fully cache the
new update before making it the latest version. Failure here is
complicated - if the page fails to load due to transient network
conditions (timeouts, 500s, etc), then it makes sense to continue
serving the existing cached version, and attempt to activate the
update on the next cycle.

If the page fails due to non-transient conditions though (400 error,
hash mismatch, etc), then the SW should consider the updated
manifest invalid, and enter a state of EXISTING_CLIENTS_ONLY.

Currently, all errors are treated as transient.

This change causes the SW to treat all errors during updates as
non-transient, which can cause the SW to unnecessarily enter a
safe mode. A future change can allow the SW to remain in normal mode
if the error is provably transient.

PR Close #21288
2018-01-10 12:18:24 -08:00
Kara Erickson 72246d48db ci: update forms approval list to include guides (#21449)
PR Close #21449
2018-01-10 12:15:50 -08:00
cexbrayat d2aa8acbe0 fix(animations): fix increment/decrement aliases example (#18323)
PR Close #18323
2018-01-10 12:06:55 -08:00
Misko Hevery ef956a20c3 build: add bazel rulse for benchmarks (#21436)
PR Close #21436
2018-01-10 11:58:53 -08:00
Kara Erickson fc3e7e0381 refactor(core): create consistent naming scheme across classes (#21403)
PR Close #21403
2018-01-10 11:15:48 -08:00
Matias Niemelä 908129e9a2 docs(core): provide note for unit test changes with the ng-star-inserted className (#21450)
PR Close #21450
2018-01-10 11:13:52 -08:00
1031 changed files with 36455 additions and 12329 deletions
+9
View File
@@ -78,6 +78,15 @@ jobs:
# See https://github.com/bazelbuild/bazel/issues/4257
- run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test
# CircleCI will allow us to go back and view/download these artifacts from past builds.
# Also we can use a service like https://buildsize.org/ to automatically track binary size of these artifacts.
- store_artifacts:
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js
destination: packages/core/test/bundling/hello_world/bundle.min.js
- store_artifacts:
path: dist/bin/packages/core/test/bundling/hello_world/bundle.min.js.brotli
destination: packages/core/test/bundling/hello_world/bundle.min.js.brotli
- save_cache:
key: *cache_key
paths:
+6
View File
@@ -207,6 +207,12 @@ groups:
conditions:
files:
- "packages/forms/*"
- "aio/content/guide/forms.md"
- "aio/content/guide/form-validation.md"
- "aio/content/guide/reactive-forms.md"
- "aio/content/examples/forms/*"
- "aio/content/examples/form-validation/*"
- "aio/content/examples/reactive-forms/*"
users:
- kara #primary
- tinayuangao #secondary
+120 -15
View File
@@ -1,31 +1,39 @@
<a name="5.2.7"></a>
## [5.2.7](https://github.com/angular/angular/compare/5.2.6...5.2.7) (2018-02-28)
<a name="6.0.0-beta.4"></a>
# [6.0.0-beta.4](https://github.com/angular/angular/compare/6.0.0-beta.3...6.0.0-beta.4) (2018-02-14)
### Bug Fixes
* **platform-server:** generate correct stylings for camel case names ([#22263](https://github.com/angular/angular/issues/22263)) ([de02a7a](https://github.com/angular/angular/commit/de02a7a)), closes [#19235](https://github.com/angular/angular/issues/19235)
* **router:** don't mutate route configs ([#22358](https://github.com/angular/angular/issues/22358)) ([8f0a064](https://github.com/angular/angular/commit/8f0a064)), closes [#22203](https://github.com/angular/angular/issues/22203)
* **upgrade:** correctly destroy nested downgraded component ([#22400](https://github.com/angular/angular/issues/22400)) ([4aef9de](https://github.com/angular/angular/commit/4aef9de)), closes [#22392](https://github.com/angular/angular/issues/22392)
* **upgrade:** correctly handle `=` bindings in `[@angular](https://github.com/angular)/upgrade` ([#22167](https://github.com/angular/angular/issues/22167)) ([6638390](https://github.com/angular/angular/commit/6638390))
* **upgrade:** fix empty transclusion content with AngularJS@>=1.5.8 ([#22167](https://github.com/angular/angular/issues/22167)) ([a9a0e27](https://github.com/angular/angular/commit/a9a0e27)), closes [#22175](https://github.com/angular/angular/issues/22175)
* **bazel:** allow TS to read ambient typings ([#21876](https://github.com/angular/angular/issues/21876)) ([b081dfe](https://github.com/angular/angular/commit/b081dfe)), closes [#21872](https://github.com/angular/angular/issues/21872)
* **bazel:** improve error message for missing assets ([#22096](https://github.com/angular/angular/issues/22096)) ([dcf64a0](https://github.com/angular/angular/commit/dcf64a0)), closes [#22095](https://github.com/angular/angular/issues/22095)
* **common:** add locale currency values ([#21783](https://github.com/angular/angular/issues/21783)) ([420cc7a](https://github.com/angular/angular/commit/420cc7a)), closes [#20385](https://github.com/angular/angular/issues/20385)
* **common:** round currencies based on decimal digits in `CurrencyPipe` ([#21783](https://github.com/angular/angular/issues/21783)) ([44154e7](https://github.com/angular/angular/commit/44154e7)), closes [#10189](https://github.com/angular/angular/issues/10189)
* **common:** weaken AsyncPipe transform signature ([#22169](https://github.com/angular/angular/issues/22169)) ([be59c3a](https://github.com/angular/angular/commit/be59c3a))
* **compiler:** make unary plus operator consistent to JavaScript ([#22154](https://github.com/angular/angular/issues/22154)) ([72f8abd](https://github.com/angular/angular/commit/72f8abd)), closes [#22089](https://github.com/angular/angular/issues/22089)
* **core:** add stacktrace in log when error during cleanup component in TestBed ([#22162](https://github.com/angular/angular/issues/22162)) ([16d1700](https://github.com/angular/angular/commit/16d1700))
* **core:** ensure initial value of QueryList length ([#21980](https://github.com/angular/angular/issues/21980)) ([#21982](https://github.com/angular/angular/issues/21982)) ([e56de10](https://github.com/angular/angular/commit/e56de10)), closes [#21980](https://github.com/angular/angular/issues/21980)
* **core:** use appropriate inert document strategy for Firefox & Safari ([#17019](https://github.com/angular/angular/issues/17019)) ([a751649](https://github.com/angular/angular/commit/a751649))
* **forms:** make Validators.email support optional controls ([#20869](https://github.com/angular/angular/issues/20869)) ([140e7c0](https://github.com/angular/angular/commit/140e7c0))
* **forms:** prevent event emission on enable/disable when emitEvent is false ([#12366](https://github.com/angular/angular/issues/12366)) ([#21018](https://github.com/angular/angular/issues/21018)) ([0bcfae7](https://github.com/angular/angular/commit/0bcfae7))
* **forms:** set state before emitting a value from ngModelChange ([#21514](https://github.com/angular/angular/issues/21514)) ([9744a1c](https://github.com/angular/angular/commit/9744a1c)), closes [#21513](https://github.com/angular/angular/issues/21513)
* **language-service:** correct instructions to install the language service ([#22000](https://github.com/angular/angular/issues/22000)) ([b37cee3](https://github.com/angular/angular/commit/b37cee3))
* **platform-browser:** add @Injectable where it was missing ([#22005](https://github.com/angular/angular/issues/22005)) ([0a1a397](https://github.com/angular/angular/commit/0a1a397))
* **platform-browser:** support 0/false/null values in transfer_state ([#22179](https://github.com/angular/angular/issues/22179)) ([6435ecd](https://github.com/angular/angular/commit/6435ecd))
### Features
<a name="5.2.6"></a>
## [5.2.6](https://github.com/angular/angular/compare/5.2.5...5.2.6) (2018-02-22)
* **bazel:** allow explicit specification of factories ([#22003](https://github.com/angular/angular/issues/22003)) ([e442881](https://github.com/angular/angular/commit/e442881))
* **compiler:** mark @NgModules in provider lists for identification at runtime ([#22005](https://github.com/angular/angular/issues/22005)) ([2d5e7d1](https://github.com/angular/angular/commit/2d5e7d1))
* **forms:** multiple validators for array method ([#20766](https://github.com/angular/angular/issues/20766)) ([941e88f](https://github.com/angular/angular/commit/941e88f)), closes [#20665](https://github.com/angular/angular/issues/20665)
* change @Injectable() to support tree-shakeable tokens ([#22005](https://github.com/angular/angular/issues/22005)) ([235a235](https://github.com/angular/angular/commit/235a235))
### Bug Fixes
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([ce5e8fa](https://github.com/angular/angular/commit/ce5e8fa)), closes [#20516](https://github.com/angular/angular/issues/20516)
* **common:** then and else template might be set to null ([#22298](https://github.com/angular/angular/issues/22298)) ([af6a056](https://github.com/angular/angular/commit/af6a056))
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([c5418c7](https://github.com/angular/angular/commit/c5418c7))
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([5ec38f2](https://github.com/angular/angular/commit/5ec38f2)), closes [#19978](https://github.com/angular/angular/issues/19978)
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([ee91de9](https://github.com/angular/angular/commit/ee91de9)), closes [#21731](https://github.com/angular/angular/issues/21731)
<a name="5.2.5"></a>
## [5.2.5](https://github.com/angular/angular/compare/5.2.4...5.2.5) (2018-02-14)
### Bug Fixes
* **aio:** update Firebase redirects and SW routes ([#21763](https://github.com/angular/angular/pull/21763)) ([#22104](https://github.com/angular/angular/pull/22104)) ([15ff7ba](https://github.com/angular/angular/commit/15ff7ba)), closes [#21377](https://github.com/angular/angular/issues/21377)
@@ -42,6 +50,36 @@
<a name="6.0.0-beta.3"></a>
# [6.0.0-beta.3](https://github.com/angular/angular/compare/6.0.0-beta.2...6.0.0-beta.3) (2018-02-07)
### Bug Fixes
* **common:** don't convert null to a string when flushing a mock request ([#21417](https://github.com/angular/angular/issues/21417)) ([8b14488](https://github.com/angular/angular/commit/8b14488)), closes [#20744](https://github.com/angular/angular/issues/20744)
* **core:** fix [#20582](https://github.com/angular/angular/issues/20582), don't need to wrap zone in location change listener ([#20640](https://github.com/angular/angular/issues/20640)) ([f791e9f](https://github.com/angular/angular/commit/f791e9f))
* **core:** fix proper propagation of subscriptions in EventEmitter ([#22016](https://github.com/angular/angular/issues/22016)) ([e81606c](https://github.com/angular/angular/commit/e81606c)), closes [#21999](https://github.com/angular/angular/issues/21999)
* **core:** should check Zone existance when scheduleMicroTask ([#20656](https://github.com/angular/angular/issues/20656)) ([3a86940](https://github.com/angular/angular/commit/3a86940))
* **forms:** publish missing types ([#19941](https://github.com/angular/angular/issues/19941)) ([2707012](https://github.com/angular/angular/commit/2707012))
* **ivy:** generate correct interpolations ([#21946](https://github.com/angular/angular/issues/21946)) ([3cc1d76](https://github.com/angular/angular/commit/3cc1d76))
* **ivy:** generate lifecycle pattern ([#21865](https://github.com/angular/angular/issues/21865)) ([f816666](https://github.com/angular/angular/commit/f816666))
* **ivy:** improve `bindV` perf and memory usage ([#21881](https://github.com/angular/angular/issues/21881)) ([0846784](https://github.com/angular/angular/commit/0846784))
* **ivy:** remove unnecessary parameter of NgOnChangesFeature ([#21879](https://github.com/angular/angular/issues/21879)) ([65cf1ad](https://github.com/angular/angular/commit/65cf1ad))
### Features
* **compiler-cli:** reflect static methods added to classes in metadata ([#21926](https://github.com/angular/angular/issues/21926)) ([eb8ddd2](https://github.com/angular/angular/commit/eb8ddd2))
* **ivy:** add canonical example of a pipe. ([#21834](https://github.com/angular/angular/issues/21834)) ([743d8bc](https://github.com/angular/angular/commit/743d8bc))
* **ivy:** add support for attributes on ng-content nodes ([#21935](https://github.com/angular/angular/issues/21935)) ([1aa2947](https://github.com/angular/angular/commit/1aa2947))
* **ivy:** memoize array literals in render3 ([#21973](https://github.com/angular/angular/issues/21973)) ([4d62be6](https://github.com/angular/angular/commit/4d62be6))
### Performance Improvements
* **ivy:** improve Uglify configuration in hello world integration test ([#21985](https://github.com/angular/angular/issues/21985)) ([7e51e52](https://github.com/angular/angular/commit/7e51e52))
<a name="5.2.4"></a>
## [5.2.4](https://github.com/angular/angular/compare/5.2.3...5.2.4) (2018-02-07)
@@ -56,6 +94,17 @@
<a name="6.0.0-beta.2"></a>
# [6.0.0-beta.2](https://github.com/angular/angular/compare/6.0.0-beta.1...6.0.0-beta.2) (2018-01-31)
### Features
* **router:** add navigationSource and restoredState to NavigationStart event ([#21728](https://github.com/angular/angular/issues/21728)) ([c40ae7f](https://github.com/angular/angular/commit/c40ae7f))
* **service-worker:** add helper script which will uninstall SW ([#21863](https://github.com/angular/angular/issues/21863)) ([b10540a](https://github.com/angular/angular/commit/b10540a))
<a name="5.2.3"></a>
## [5.2.3](https://github.com/angular/angular/compare/5.2.2...5.2.3) (2018-01-31)
@@ -73,6 +122,32 @@
<a name="6.0.0-beta.1"></a>
# [6.0.0-beta.1](https://github.com/angular/angular/compare/6.0.0-beta.0...6.0.0-beta.1) (2018-01-25)
### Bug Fixes
* **common:** A null value should remove the style on IE ([#21679](https://github.com/angular/angular/issues/21679)) ([7d49443](https://github.com/angular/angular/commit/7d49443)), closes [#21064](https://github.com/angular/angular/issues/21064)
* avoid triggering a cli bug ([#21611](https://github.com/angular/angular/issues/21611)) ([0eabd07](https://github.com/angular/angular/commit/0eabd07))
* **common:** don't remove special characters when extracting CLDR data ([#21626](https://github.com/angular/angular/issues/21626)) ([135a282](https://github.com/angular/angular/commit/135a282))
* **common:** extract plural function from i18n locale data files for TS 2.6 ([#21626](https://github.com/angular/angular/issues/21626)) ([97b18b2](https://github.com/angular/angular/commit/97b18b2)), closes [#21608](https://github.com/angular/angular/issues/21608)
* **common:** fallback to last defined value for named date and time formats ([#21299](https://github.com/angular/angular/issues/21299)) ([879756d](https://github.com/angular/angular/commit/879756d)), closes [#21282](https://github.com/angular/angular/issues/21282)
* **compiler:** add support for marker tags in xliff serializers ([#21250](https://github.com/angular/angular/issues/21250)) ([f74130c](https://github.com/angular/angular/commit/f74130c)), closes [#21078](https://github.com/angular/angular/issues/21078)
* **compiler:** Don't strip `/*# sourceURL ... */` ([#16088](https://github.com/angular/angular/issues/16088)) ([5f681f9](https://github.com/angular/angular/commit/5f681f9))
* **compiler:** fix ICU select messages to use male/female/other ([#21713](https://github.com/angular/angular/issues/21713)) ([cb5090c](https://github.com/angular/angular/commit/cb5090c))
* **compiler-cli:** do not fold errors past calls in the collector ([#21708](https://github.com/angular/angular/issues/21708)) ([dd86790](https://github.com/angular/angular/commit/dd86790))
* **compiler-cli:** do not lower expressions in non-modules ([#21649](https://github.com/angular/angular/issues/21649)) ([7f93aad](https://github.com/angular/angular/commit/7f93aad))
* **router:** don't use ParamsInheritanceStrategy in declarations ([#21574](https://github.com/angular/angular/issues/21574)) ([925e654](https://github.com/angular/angular/commit/925e654)), closes [#21456](https://github.com/angular/angular/issues/21456)
### Features
* **compiler:** implement "enableIvy" compiler option ([#21427](https://github.com/angular/angular/issues/21427)) ([64d16de](https://github.com/angular/angular/commit/64d16de))
* **core:** optional generic type for ElementRef ([#20765](https://github.com/angular/angular/issues/20765)) ([d3d9aac](https://github.com/angular/angular/commit/d3d9aac)), closes [#13139](https://github.com/angular/angular/issues/13139)
<a name="5.2.2"></a>
## [5.2.2](https://github.com/angular/angular/compare/5.2.1...5.2.2) (2018-01-25)
@@ -92,6 +167,36 @@
<a name="6.0.0-beta.0"></a>
# [6.0.0-beta.0](https://github.com/angular/angular/compare/5.2.0...6.0.0-beta.0) (2018-01-17)
### Bug Fixes
* **animations:** fix increment/decrement aliases example ([#18323](https://github.com/angular/angular/issues/18323)) ([d2aa8ac](https://github.com/angular/angular/commit/d2aa8ac))
* **benchpress:** should still support selenium_webdriver < 3.6.0 ([#21477](https://github.com/angular/angular/issues/21477)) ([9b84a32](https://github.com/angular/angular/commit/9b84a32))
* **common:** set correct timezone for ISO8601 dates in Safari ([#21506](https://github.com/angular/angular/issues/21506)) ([05208b8](https://github.com/angular/angular/commit/05208b8)), closes [#21491](https://github.com/angular/angular/issues/21491)
* **compiler:** cache external reference resolution ([#21359](https://github.com/angular/angular/issues/21359)) ([e3e2fc0](https://github.com/angular/angular/commit/e3e2fc0))
* **compiler:** make `.ngsummary.json` files idempotent ([#21448](https://github.com/angular/angular/issues/21448)) ([e64b1e9](https://github.com/angular/angular/commit/e64b1e9))
* **core:** fix chained http call ([#20924](https://github.com/angular/angular/issues/20924)) ([7e3f9a4](https://github.com/angular/angular/commit/7e3f9a4)), closes [#20921](https://github.com/angular/angular/issues/20921)
* **ivy:** Add workaround for AJD in google3 ([#21488](https://github.com/angular/angular/issues/21488)) ([6af3672](https://github.com/angular/angular/commit/6af3672))
* **language-service:** Clear caches when program changes ([#21337](https://github.com/angular/angular/issues/21337)) ([43e1520](https://github.com/angular/angular/commit/43e1520)), closes [#19405](https://github.com/angular/angular/issues/19405)
* **service-worker:** properly handle invalid hashes in all scenarios ([#21288](https://github.com/angular/angular/issues/21288)) ([3951098](https://github.com/angular/angular/commit/3951098))
### Features
* **bazel:** allow ng_module rules to control whether type checking is enabled ([#21460](https://github.com/angular/angular/issues/21460)) ([cffa0fe](https://github.com/angular/angular/commit/cffa0fe))
* **core:** add binding name to content changed error ([#20352](https://github.com/angular/angular/issues/20352)) ([d3bf54b](https://github.com/angular/angular/commit/d3bf54b))
* **forms:** handle string with and without line boundary on pattern validator ([#19256](https://github.com/angular/angular/issues/19256)) ([54bf179](https://github.com/angular/angular/commit/54bf179))
### Performance Improvements
* **ivy:** add missing dom element in render3_function tree benchmark ([#21476](https://github.com/angular/angular/issues/21476)) ([9b5a485](https://github.com/angular/angular/commit/9b5a485))
<a name="5.2.1"></a>
## [5.2.1](https://github.com/angular/angular/compare/5.2.0...5.2.1) (2018-01-17)
-12
View File
@@ -1,12 +0,0 @@
# Contributor Code of Conduct
## Version 0.3b-angular
As contributors and maintainers of the Angular project, we pledge to respect everyone who contributes by posting issues, updating documentation, submitting pull requests, providing feedback in comments, and any other activities.
Communication through any of Angular's channels (GitHub, Gitter, IRC, mailing lists, Google+, Twitter, etc.) must be constructive and never resort to personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
We promise to extend courtesy and respect to everyone involved in this project regardless of gender, gender identity, sexual orientation, disability, age, race, ethnicity, religion, or level of experience. We expect anyone contributing to the Angular project to do the same.
If any member of the community violates this code of conduct, the maintainers of the Angular project may take action, removing issues, comments, and PRs or blocking accounts as deemed appropriate.
If you are subject to or witness unacceptable behavior, or have any other concerns, please email us at [conduct@angular.io](mailto:conduct@angular.io).
+28 -11
View File
@@ -16,13 +16,13 @@ load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_reposi
check_bazel_version("0.9.0")
node_repositories(package_json = ["//:package.json"])
RULES_TYPESCRIPT_VERSION = "d3cc5cd72d89aee0e4c2553ae1b99c707ecbef4e"
RULES_TYPESCRIPT_VERSION = "0.10.1"
http_archive(
name = "build_bazel_rules_typescript",
url = "https://github.com/bazelbuild/rules_typescript/archive/%s.zip" % RULES_TYPESCRIPT_VERSION,
strip_prefix = "rules_typescript-%s" % RULES_TYPESCRIPT_VERSION,
sha256 = "a233fcca41c3e59f639ac71c396edb30e9e9716cf8ed5fb20b51ff8910d5d895",
sha256 = "a2c81776a4a492ff9f878f9705639f5647bef345f7f3e1da09c9eeb8dec80485",
)
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
@@ -34,16 +34,13 @@ local_repository(
path = "node_modules/rxjs/src",
)
# This commit matches the version of buildifier in angular/ngcontainer
# If you change this, also check if it matches the version in the angular/ngcontainer
# version in /.circleci/config.yml
BAZEL_BUILDTOOLS_VERSION = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71"
http_archive(
git_repository(
name = "com_github_bazelbuild_buildtools",
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
sha256 = "dad19224258ed67cbdbae9b7befb785c3b966e5a33b04b3ce58ddb7824b97d73",
remote = "https://github.com/bazelbuild/buildtools.git",
# Note, this commit matches the version of buildifier in angular/ngcontainer
# If you change this, also check if it matches the version in the angular/ngcontainer
# version in /.circleci/config.yml
commit = "b3b620e8bcff18ed3378cd3f35ebeb7016d71f71",
)
http_archive(
@@ -65,3 +62,23 @@ http_archive(
strip_prefix = "bazel-9755c72b48866ed034bd28aa033e9abd27431b1e",
sha256 = "5b8443fc3481b5fcd9e7f348e1dd93c1397f78b223623c39eb56494c55f41962",
)
# We have a source dependency on the Devkit repository, because it's built with
# Bazel.
# This allows us to edit sources and have the effect appear immediately without
# re-packaging or "npm link"ing.
# Even better, things like aspects will visit the entire graph including
# ts_library rules in the devkit repository.
http_archive(
name = "angular_devkit",
url = "https://github.com/angular/devkit/archive/v0.3.1.zip",
strip_prefix = "devkit-0.3.1",
sha256 = "31d4b597fe9336650acf13df053c1c84dcbe9c29c6a833bcac3819cd3fd8cad3",
)
http_archive(
name = "org_brotli",
url = "https://github.com/google/brotli/archive/v1.0.2.zip",
strip_prefix = "brotli-1.0.2",
sha256 = "b43d5d6bc40f2fa6c785b738d86c6bbe022732fe25196ebbe43b9653a025920d",
)
+3 -3
View File
@@ -39,7 +39,7 @@
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
"config": "tests/e2e/protractor.conf.js"
}
},
"lint": [
@@ -50,12 +50,12 @@
"project": "src/tsconfig.spec.json"
},
{
"project": "e2e/tsconfig.e2e.json"
"project": "tests/e2e/tsconfig.e2e.json"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
"config": "src/karma.conf.js"
}
},
"defaults": {
@@ -1,131 +0,0 @@
import { Component, Output, OnInit, EventEmitter, NgModule } from '@angular/core';
import { Observable } from 'rxjs/Observable';
// #docregion eventemitter
@Component({
selector: 'zippy',
template: `
<div class="zippy">
<div (click)="toggle()">Toggle</div>
<div [hidden]="!visible">
<ng-content></ng-content>
</div>
</div>`})
export class ZippyComponent {
visible = true;
@Output() open = new EventEmitter<any>();
@Output() close = new EventEmitter<any>();
toggle() {
this.visible = !this.visible;
if (this.visible) {
this.open.emit(null);
} else {
this.close.emit(null);
}
}
}
// #enddocregion eventemitter
// #docregion pipe
@Component({
selector: 'async-observable-pipe',
template: `<div><code>observable|async</code>:
Time: {{ time | async }}</div>`
})
export class AsyncObservablePipeComponent {
time = new Observable(observer =>
setInterval(() => observer.next(new Date().toString()), 1000)
);
}
// #enddocregion pipe
// #docregion router
import { Router, NavigationStart } from '@angular/router';
import { filter } from 'rxjs/operators';
@Component({
selector: 'app-routable',
templateUrl: './routable.component.html',
styleUrls: ['./routable.component.css']
})
export class Routable1Component implements OnInit {
navStart: Observable<NavigationStart>;
constructor(private router: Router) {
// Create a new Observable the publishes only the NavigationStart event
this.navStart = router.events.pipe(
filter(evt => evt instanceof NavigationStart)
) as Observable<NavigationStart>;
}
ngOnInit() {
this.navStart.subscribe(evt => console.log('Navigation Started!'));
}
}
// #enddocregion router
// #docregion activated_route
import { ActivatedRoute } from '@angular/router';
@Component({
selector: 'app-routable',
templateUrl: './routable.component.html',
styleUrls: ['./routable.component.css']
})
export class Routable2Component implements OnInit {
constructor(private activatedRoute: ActivatedRoute) {}
ngOnInit() {
this.activatedRoute.url
.subscribe(url => console.log('The URL changed to: ' + url));
}
}
// #enddocregion activated_route
// #docregion forms
import { FormGroup } from '@angular/forms';
@Component({
selector: 'my-component',
template: 'MyComponent Template'
})
export class MyComponent implements OnInit {
nameChangeLog: string[] = [];
heroForm: FormGroup;
ngOnInit() {
this.logNameChange();
}
logNameChange() {
const nameControl = this.heroForm.get('name');
nameControl.valueChanges.forEach(
(value: string) => this.nameChangeLog.push(value)
);
}
}
// #enddocregion forms
@NgModule({
declarations:
[ZippyComponent, AsyncObservablePipeComponent, Routable1Component, Routable2Component, MyComponent]
})
export class AppModule {
}
@@ -1,66 +0,0 @@
import { Observable } from 'rxjs/Observable';
// #docregion subscriber
// This function runs when subscribe() is called
function sequenceSubscriber(observer) {
// synchronously deliver 1, 2, and 3, then complete
observer.next(1);
observer.next(2);
observer.next(3);
observer.complete();
// unsubscribe function doesn't need to do anything in this
// because values are delivered synchronously
return {unsubscribe() {}};
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
// execute the Observable and print the result of each notification
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// 1
// 2
// 3
// Finished sequence
// #enddocregion subscriber
// #docregion fromevent
function fromEvent(target, eventName) {
return new Observable((observer) => {
const handler = (e) => observer.next(e);
// Add the event handler to the target
target.addEventListener(eventName, handler);
return () => {
// Detach the event handler from the target
target.removeEventListener(eventName, handler);
};
});
}
// #enddocregion fromevent
// #docregion fromevent_use
const ESC_KEY = 27;
const nameInput = document.getElementById('name') as HTMLInputElement;
const subscription = fromEvent(nameInput, 'keydown')
.subscribe((e: KeyboardEvent) => {
if (e.keyCode === ESC_KEY) {
nameInput.value = '';
}
});
// #enddocregion fromevent_use
@@ -1,32 +0,0 @@
import { Observable } from 'rxjs/Observable';
// #docregion
// Create an Observable that will start listening to geolocation updates
// when a consumer subscribes.
const locations = new Observable((observer) => {
// Get the next and error callbacks. These will be passed in when
// the consumer subscribes.
const {next, error} = observer;
let watchId;
// Simple geolocation API check provides values to publish
if ('geolocation' in navigator) {
watchId = navigator.geolocation.watchPosition(next, error);
} else {
error('Geolocation not available');
}
// When the consumer unsubscribes, clean up data ready for next subscription.
return {unsubscribe() { navigator.geolocation.clearWatch(watchId); }};
});
// Call subscribe() to start listening for updates.
const locationsSubscription = locations.subscribe({
next(position) { console.log('Current Position: ', position); },
error(msg) { console.log('Error Getting Location: ', msg); }
});
// Stop listening for location after 10 seconds
setTimeout(() => { locationsSubscription.unsubscribe(); }, 10000);
// #enddocregion
@@ -1,5 +0,0 @@
import './geolocation';
import './subscribing';
import './creating';
import './multicasting';
@@ -1,155 +0,0 @@
import { Observable } from 'rxjs/Observable';
// #docregion delay_sequence
function sequenceSubscriber(observer) {
const seq = [1, 2, 3];
let timeoutId;
// Will run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(arr, idx) {
timeoutId = setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doSequence(arr, idx++);
}
}, 1000);
}
doSequence(seq, 0);
// Unsubscribe should clear the timeout to stop execution
return {unsubscribe() {
clearTimeout(timeoutId);
}};
}
// Create a new Observable that will deliver the above sequence
const sequence = new Observable(sequenceSubscriber);
sequence.subscribe({
next(num) { console.log(num); },
complete() { console.log('Finished sequence'); }
});
// Logs:
// (at 1 second): 1
// (at 2 seconds): 2
// (at 3 seconds): 3
// (at 3 seconds): Finished sequence
// #enddocregion delay_sequence
// #docregion subscribe_twice
// Subscribe starts the clock, and will emit after 1 second
sequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1/2 second, subscribe again.
setTimeout(() => {
sequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 1.5 seconds): 2nd subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2.5 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3.5 seconds): 2nd subscribe: 3
// (at 3.5 seconds): 2nd sequence finished
// #enddocregion subscribe_twice
// #docregion multicast_sequence
function multicastSequenceSubscriber() {
const seq = [1, 2, 3];
// Keep track of each observer (one for every active subscription)
const observers = [];
// Still a single timeoutId because there will only ever be one
// set of values being generated, multicasted to each subscriber
let timeoutId;
// Return the subscriber function (runs when subscribe()
// function is invoked)
return (observer) => {
observers.push(observer);
// When this is the first subscription, start the sequence
if (observers.length === 1) {
timeoutId = doSequence({
next(val) {
// Iterate through observers and notify all subscriptions
observers.forEach(obs => obs.next(val));
},
complete() {
// Notify all complete callbacks
observers.forEach(obs => obs.complete());
}
}, seq, 0);
}
return {
unsubscribe() {
// Remove from the observers array so it's no longer notified
observers.splice(observers.indexOf(observer), 1);
// If there's no more listeners, do cleanup
if (observers.length === 0) {
clearTimeout(timeoutId);
}
}
};
};
}
// Run through an array of numbers, emitting one value
// per second until it gets to the end of the array.
function doSequence(observer, arr, idx) {
return setTimeout(() => {
observer.next(arr[idx]);
if (idx === arr.length - 1) {
observer.complete();
} else {
doSequence(observer, arr, idx++);
}
}, 1000);
}
// Create a new Observable that will deliver the above sequence
const multicastSequence = new Observable(multicastSequenceSubscriber);
// Subscribe starts the clock, and begins to emit after 1 second
multicastSequence.subscribe({
next(num) { console.log('1st subscribe: ' + num); },
complete() { console.log('1st sequence finished.'); }
});
// After 1 1/2 seconds, subscribe again (should "miss" the first value).
setTimeout(() => {
multicastSequence.subscribe({
next(num) { console.log('2nd subscribe: ' + num); },
complete() { console.log('2nd sequence finished.'); }
});
}, 1500);
// Logs:
// (at 1 second): 1st subscribe: 1
// (at 2 seconds): 1st subscribe: 2
// (at 2 seconds): 2nd subscribe: 2
// (at 3 seconds): 1st subscribe: 3
// (at 3 seconds): 1st sequence finished
// (at 3 seconds): 2nd subscribe: 3
// (at 3 seconds): 2nd sequence finished
// #enddocregion multicast_sequence
@@ -1,33 +0,0 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion observer
// Create simple observable that emits three values
const myObservable = Observable.of(1, 2, 3);
// Create observer object
const myObserver = {
next: x => console.log('Observer got a next value: ' + x),
error: err => console.error('Observer got an error: ' + err),
complete: () => console.log('Observer got a complete notification'),
};
// Execute with the observer object
myObservable.subscribe(myObserver);
// Logs:
// Observer got a next value: 1
// Observer got a next value: 2
// Observer got a next value: 3
// Observer got a complete notification
// #enddocregion observer
// #docregion sub_fn
myObservable.subscribe(
x => console.log('Observer got a next value: ' + x),
err => console.error('Observer got an error: ' + err),
() => console.log('Observer got a complete notification')
);
// #enddocregion sub_fn
@@ -1,26 +0,0 @@
import { ajax } from 'rxjs/observable/dom/ajax';
import { range } from 'rxjs/observable/range';
import { timer } from 'rxjs/observable/timer';
import { pipe } from 'rxjs/util/pipe';
import { retryWhen, zip, map, mergeMap } from 'rxjs/operators';
function backoff(maxTries, ms) {
return pipe(
retryWhen(attempts => range(1, maxTries)
.pipe(
zip(attempts, (i) => i),
map(i => i * i),
mergeMap(i => timer(i * ms))
)
)
);
}
ajax('/api/endpoint')
.pipe(backoff(3, 250))
.subscribe(data => handleData(data));
function handleData(data) {
// ...
}
@@ -1,18 +0,0 @@
import { fromEvent } from 'rxjs/observable/fromEvent';
import { ajax } from 'rxjs/observable/dom/ajax';
import { map, filter, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators';
const searchBox = document.getElementById('search-box');
const typeahead = fromEvent(searchBox, 'input').pipe(
map((e: KeyboardEvent) => e.target.value),
filter(text => text.length > 2),
debounceTime(10),
distinctUntilChanged(),
switchMap(() => ajax('/api/endpoint'))
);
typeahead.subscribe(data => {
// Handle the data from the API
});
@@ -1,26 +0,0 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { ajax } from 'rxjs/observable/dom/ajax';
import { map, catchError } from 'rxjs/operators';
// Return "response" from the API. If an error happens,
// return an empty array.
const apiData = ajax('/api/data').pipe(
map(res => {
if (!res.response) {
throw new Error('Value expected!');
}
return res.response;
}),
catchError(err => Observable.of([]))
);
apiData.subscribe({
next(x) { console.log('data: ', x); },
error(err) { console.log('errors already caught... will not run'); }
});
// #enddocregion
@@ -1,20 +0,0 @@
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'app-stopwatch',
templateUrl: './stopwatch.component.html'
})
export class StopwatchComponent {
stopwatchValue: number;
stopwatchValue$: Observable<number>;
start() {
this.stopwatchValue$.subscribe(num =>
this.stopwatchValue = num
);
}
}
@@ -1,25 +0,0 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { pipe } from 'rxjs/util/pipe';
import { filter, map } from 'rxjs/operators';
const nums = Observable.of(1, 2, 3, 4, 5);
// Create a function that accepts an Observable.
const squareOddVals = pipe(
filter(n => n % 2),
map(n => n * n)
);
// Create an Observable that will run the filter and map functions
const squareOdd = squareOddVals(nums);
// Suscribe to run the combined functions
squareOdd.subscribe(x => console.log(x));
// #enddocregion
@@ -1,18 +0,0 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { filter } from 'rxjs/operators/filter';
import { map } from 'rxjs/operators/map';
const squareOdd = Observable.of(1, 2, 3, 4, 5)
.pipe(
filter(n => n % 2),
map(n => n * n)
);
// Subscribe to get values
squareOdd.subscribe(x => console.log(x));
// #enddocregion
@@ -1,21 +0,0 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { map } from 'rxjs/operators';
const nums = Observable.of(1, 2, 3);
const squareValues = map((val: number) => val * val);
const squaredNums = squareValues(nums);
squaredNums.subscribe(x => console.log(x));
// Logs
// 1
// 4
// 9
// #enddocregion
@@ -1,27 +0,0 @@
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
// #docregion
import { ajax } from 'rxjs/observable/dom/ajax';
import { map, retry, catchError } from 'rxjs/operators';
const apiData = ajax('/api/data').pipe(
retry(3), // Retry up to 3 times before failing
map(res => {
if (!res.response) {
throw new Error('Value expected!');
}
return res.response;
}),
catchError(err => Observable.of([]))
);
apiData.subscribe({
next(x) { console.log('data: ', x); },
error(err) { console.log('errors already caught... will not run'); }
});
// #enddocregion
@@ -1,65 +0,0 @@
// #docregion promise
import { fromPromise } from 'rxjs/observable/fromPromise';
// Create an Observable out of a promise
const data = fromPromise(fetch('/api/endpoint'));
// Subscribe to begin listening for async result
data.subscribe({
next(response) { console.log(response); },
error(err) { console.error('Error: ' + err); },
complete() { console.log('Completed'); }
});
// #enddocregion promise
// #docregion interval
import { interval } from 'rxjs/observable/interval';
// Create an Observable that will publish a value on an interval
const secondsCounter = interval(1000);
// Subscribe to begin publishing values
secondsCounter.subscribe(n =>
console.log(`It's been ${n} seconds since subscribing!`));
// #enddocregion interval
// #docregion event
import { fromEvent } from 'rxjs/observable/fromEvent';
const el = document.getElementById('my-element');
// Create an Observable that will publish mouse movements
const mouseMoves = fromEvent(el, 'mousemove');
// Subscribe to start listening for mouse-move events
const subscription = mouseMoves.subscribe((evt: MouseEvent) => {
// Log coords of mouse movements
console.log(`Coords: ${evt.clientX} X ${evt.clientY}`);
// When the mouse is over the upper-left of the screen,
// unsubscribe to stop listening for mouse movements
if (evt.clientX < 40 && evt.clientY < 40) {
subscription.unsubscribe();
}
});
// #enddocregion event
// #docregion ajax
import { ajax } from 'rxjs/observable/dom/ajax';
// Create an Observable that will create an AJAX request
const apiData = ajax('/api/data');
// Subscribe to create the request
apiData.subscribe(res => console.log(res.status, res.response));
// #enddocregion ajax
@@ -1,5 +1,5 @@
{
"description": "Testing - specs",
"description": "Testing - app.specs",
"files":[
"src/styles.css",
@@ -0,0 +1,5 @@
// #docplaster
// #docregion
describe('1st tests', () => {
it('true is true', () => expect(true).toBe(true));
});
@@ -1,8 +1,9 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { AboutComponent } from './about.component';
import { HighlightDirective } from '../shared/highlight.directive';
import { HighlightDirective } from './shared/highlight.directive';
let fixture: ComponentFixture<AboutComponent>;
@@ -18,8 +19,8 @@ describe('AboutComponent (highlightDirective)', () => {
});
it('should have skyblue <h2>', () => {
const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
const bgColor = h2.style.backgroundColor;
const de = fixture.debugElement.query(By.css('h2'));
const bgColor = de.nativeElement.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
// #enddocregion tests
@@ -3,8 +3,7 @@ import { Component } from '@angular/core';
@Component({
template: `
<h2 highlight="skyblue">About</h2>
<h3>Quote of the day:</h3>
<twain-quote></twain-quote>
`
<p>All about this sample</p>`
})
export class AboutComponent { }
@@ -1,76 +0,0 @@
// #docplaster
// #docregion
import { TestBed, async } from '@angular/core/testing';
// #enddocregion
import { AppComponent } from './app-initial.component';
/*
// #docregion
import { AppComponent } from './app.component';
describe('AppComponent', () => {
// #enddocregion
*/
describe('AppComponent (initial CLI version)', () => {
// #docregion
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});
// #enddocregion
/// As it should be
import { DebugElement } from '@angular/core';
import { ComponentFixture } from '@angular/core/testing';
describe('AppComponent (initial CLI version - as it should be)', () => {
let app: AppComponent;
let de: DebugElement;
let fixture: ComponentFixture<AppComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
});
fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
de = fixture.debugElement;
});
it('should create the app', () => {
expect(app).toBeDefined();
});
it(`should have as title 'app'`, () => {
expect(app.title).toEqual('app');
});
it('should render title in an h1 tag', () => {
fixture.detectChanges();
expect(de.nativeElement.querySelector('h1').textContent)
.toContain('Welcome to app!');
});
});
@@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AboutComponent } from './about/about.component';
import { AboutComponent } from './about.component';
@NgModule({
imports: [
@@ -1,11 +1,11 @@
<!-- #docregion -->
<app-banner></app-banner>
<app-welcome></app-welcome>
<!-- #docregion links -->
<nav>
<a routerLink="/dashboard">Dashboard</a>
<a routerLink="/heroes">Heroes</a>
<a routerLink="/about">About</a>
</nav>
<!-- #enddocregion links -->
<router-outlet></router-outlet>
@@ -4,11 +4,11 @@
import { async, ComponentFixture, fakeAsync, TestBed, tick,
} from '@angular/core/testing';
import { asyncData } from '../testing';
import { RouterTestingModule } from '@angular/router/testing';
import { SpyLocation } from '@angular/common/testing';
import { click } from '../testing';
// r - for relatively obscure router symbols
import * as r from '@angular/router';
import { Router, RouterLinkWithHref } from '@angular/router';
@@ -17,15 +17,11 @@ import { By } from '@angular/platform-browser';
import { DebugElement, Type } from '@angular/core';
import { Location } from '@angular/common';
import { click } from '../testing';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { AboutComponent } from './about/about.component';
import { AboutComponent } from './about.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { TwainService } from './twain/twain.service';
import { HeroService, TestHeroService } from './model/testing/test-hero.service';
import { TwainService } from './shared/twain.service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
@@ -35,19 +31,15 @@ let location: SpyLocation;
describe('AppComponent & RouterTestingModule', () => {
beforeEach(async(() => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ],
providers: [
{ provide: HeroService, useClass: TestHeroService }
]
imports: [ AppModule, RouterTestingModule ]
})
.compileComponents();
}));
it('should navigate to "Dashboard" immediately', fakeAsync(() => {
createComponent();
tick(); // wait for async data to arrive
expect(location.path()).toEqual('/dashboard', 'after initialNavigation()');
expectElementOf(DashboardComponent);
}));
@@ -72,7 +64,7 @@ describe('AppComponent & RouterTestingModule', () => {
}));
// Can't navigate to lazy loaded modules with this technique
xit('should navigate to "Heroes" on click (not working yet)', fakeAsync(() => {
xit('should navigate to "Heroes" on click', fakeAsync(() => {
createComponent();
page.heroesLinkDe.nativeElement.click();
advance();
@@ -92,9 +84,9 @@ import { HeroListComponent } from './hero/hero-list.component';
let loader: SpyNgModuleFactoryLoader;
///////// Can't get lazy loaded Heroes to work yet
xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
xdescribe('AppComponent & Lazy Loading', () => {
beforeEach(async(() => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ]
})
@@ -103,11 +95,14 @@ xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
beforeEach(fakeAsync(() => {
createComponent();
loader = TestBed.get(NgModuleFactoryLoader);
loader.stubbedModules = { expected: HeroModule };
loader = TestBed.get(NgModuleFactoryLoader);
loader.stubbedModules = {expected: HeroModule};
router.resetConfig([{path: 'heroes', loadChildren: 'expected'}]);
}));
it('dummy', () => expect(true).toBe(true) );
it('should navigate to "Heroes" on click', async(() => {
page.heroesLinkDe.nativeElement.click();
advance();
@@ -115,24 +110,25 @@ xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
expectElementOf(HeroListComponent);
}));
it('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
xit('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
location.go('/heroes');
advance();
expectPathToBe('/heroes');
expectElementOf(HeroListComponent);
page.expectEvents([
[r.NavigationStart, '/heroes'], [r.RoutesRecognized, '/heroes'],
[r.NavigationEnd, '/heroes']
]);
}));
});
////// Helpers /////////
/**
* Advance to the routed page
* Wait a tick, then detect changes, and tick again
*/
/** Wait a tick, then detect changes */
function advance(): void {
tick(); // wait while navigating
fixture.detectChanges(); // update view
tick(); // wait for async data to arrive
tick();
fixture.detectChanges();
}
function createComponent() {
@@ -144,8 +140,8 @@ function createComponent() {
router = injector.get(Router);
router.initialNavigation();
spyOn(injector.get(TwainService), 'getQuote')
// fake fast async observable
.and.returnValue(asyncData('Test Quote'));
.and.returnValue(Promise.resolve('Test Quote')); // fakes it
advance();
page = new Page();
@@ -155,6 +151,7 @@ class Page {
aboutLinkDe: DebugElement;
dashboardLinkDe: DebugElement;
heroesLinkDe: DebugElement;
recordedEvents: any[] = [];
// for debugging
comp: AppComponent;
@@ -162,7 +159,17 @@ class Page {
router: Router;
fixture: ComponentFixture<AppComponent>;
expectEvents(pairs: any[]) {
const events = this.recordedEvents;
expect(events.length).toEqual(pairs.length, 'actual/expected events length mismatch');
for (let i = 0; i < events.length; ++i) {
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name, 'unexpected event name');
expect((<any>events[i]).url).toBe(pairs[i][1], 'unexpected event url');
}
}
constructor() {
router.events.subscribe(e => this.recordedEvents.push(e));
const links = fixture.debugElement.queryAll(By.directive(RouterLinkWithHref));
this.aboutLinkDe = links[2];
this.dashboardLinkDe = links[0];
@@ -1,67 +1,69 @@
// #docplaster
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { async, ComponentFixture, TestBed
} from '@angular/core/testing';
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { DebugElement } from '@angular/core';
import { By } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { RouterLinkDirectiveStub } from '../testing';
// #docregion setup-schemas
import { NO_ERRORS_SCHEMA } from '@angular/core';
// #enddocregion setup-schemas
// #docregion setup-stubs-w-imports
import { Component } from '@angular/core';
// #docregion setup-schemas
import { AppComponent } from './app.component';
// #enddocregion setup-schemas
import { BannerComponent } from './banner.component';
import { RouterLinkStubDirective } from '../testing';
// #docregion setup-schemas
import { RouterOutletStubComponent } from '../testing';
// #docregion component-stubs
@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {}
// #enddocregion setup-schemas
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
// #enddocregion component-stubs
// #enddocregion setup-stubs-w-imports
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
describe('AppComponent & TestModule', () => {
beforeEach(async(() => {
// #docregion testbed-stubs
// #docregion setup-stubs, setup-stubs-w-imports
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
RouterLinkDirectiveStub,
BannerStubComponent,
RouterOutletStubComponent,
WelcomeStubComponent
BannerComponent, WelcomeStubComponent,
RouterLinkStubDirective, RouterOutletStubComponent
]
})
// #enddocregion testbed-stubs
.compileComponents().then(() => {
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
// #enddocregion setup-stubs, setup-stubs-w-imports
tests();
});
//////// Testing w/ NO_ERRORS_SCHEMA //////
describe('AppComponent & NO_ERRORS_SCHEMA', () => {
beforeEach(async(() => {
// #docregion no-errors-schema, mixed-setup
// #docregion setup-schemas
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent,
// #enddocregion no-errors-schema
BannerStubComponent,
// #docregion no-errors-schema
RouterLinkDirectiveStub
],
schemas: [ NO_ERRORS_SCHEMA ]
declarations: [ AppComponent, RouterLinkStubDirective ],
schemas: [ NO_ERRORS_SCHEMA ]
})
// #enddocregion no-errors-schema, mixed-setup
.compileComponents().then(() => {
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
// #enddocregion setup-schemas
tests();
});
@@ -73,7 +75,7 @@ import { AppRoutingModule } from './app-routing.module';
describe('AppComponent & AppModule', () => {
beforeEach(async(() => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ AppModule ]
@@ -86,7 +88,7 @@ describe('AppComponent & AppModule', () => {
imports: [ AppRoutingModule ]
},
add: {
declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ]
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ]
}
})
@@ -102,40 +104,40 @@ describe('AppComponent & AppModule', () => {
});
function tests() {
let routerLinks: RouterLinkDirectiveStub[];
let links: RouterLinkStubDirective[];
let linkDes: DebugElement[];
// #docregion test-setup
beforeEach(() => {
fixture.detectChanges(); // trigger initial data binding
// trigger initial data binding
fixture.detectChanges();
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkDirectiveStub));
.queryAll(By.directive(RouterLinkStubDirective));
// get attached link directive instances
// using each DebugElement's injector
routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
// get the attached link directive instances using the DebugElement injectors
links = linkDes
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
});
// #enddocregion test-setup
it('can instantiate the component', () => {
it('can instantiate it', () => {
expect(comp).not.toBeNull();
});
// #docregion tests
it('can get RouterLinks from template', () => {
expect(routerLinks.length).toBe(3, 'should have 3 routerLinks');
expect(routerLinks[0].linkParams).toBe('/dashboard');
expect(routerLinks[1].linkParams).toBe('/heroes');
expect(routerLinks[2].linkParams).toBe('/about');
expect(links.length).toBe(3, 'should have 3 links');
expect(links[0].linkParams).toBe('/dashboard', '1st link should go to Dashboard');
expect(links[1].linkParams).toBe('/heroes', '2nd link should go to Heroes');
});
it('can click Heroes link in template', () => {
const heroesLinkDe = linkDes[1]; // heroes link DebugElement
const heroesLink = routerLinks[1]; // heroes link directive
const heroesLinkDe = linkDes[1];
const heroesLink = links[1];
expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');
expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet');
heroesLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();
@@ -1,50 +1,29 @@
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { HttpClientModule } from '@angular/common/http';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { AboutComponent } from './about/about.component';
import { BannerComponent } from './banner/banner.component';
import { HeroService } from './model/hero.service';
import { AboutComponent } from './about.component';
import { BannerComponent } from './banner.component';
import { UserService } from './model/user.service';
import { TwainComponent } from './twain/twain.component';
import { TwainService } from './twain/twain.service';
import { WelcomeComponent } from './welcome/welcome.component';
import { HeroService } from './model/hero.service';
import { TwainService } from './shared/twain.service';
import { WelcomeComponent } from './welcome.component';
import { DashboardModule } from './dashboard/dashboard.module';
import { SharedModule } from './shared/shared.module';
import { HttpClientInMemoryWebApiModule } from 'angular-in-memory-web-api';
import { InMemoryDataService } from './in-memory-data.service';
@NgModule({
imports: [
BrowserModule,
DashboardModule,
AppRoutingModule,
SharedModule,
HttpClientModule,
// The HttpClientInMemoryWebApiModule module intercepts HTTP requests
// and returns simulated server responses.
// Remove it when a real server is ready to receive requests.
HttpClientInMemoryWebApiModule.forRoot(
InMemoryDataService, { dataEncapsulation: false }
)
SharedModule
],
providers: [
HeroService,
TwainService,
UserService
],
declarations: [
AppComponent,
AboutComponent,
BannerComponent,
TwainComponent,
WelcomeComponent ],
bootstrap: [ AppComponent ]
providers: [ HeroService, TwainService, UserService ],
declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }
@@ -1,8 +1,7 @@
// tslint:disable-next-line:no-unused-variable
import { async, fakeAsync, tick } from '@angular/core/testing';
import { of } from 'rxjs/observable/of';
import { delay } from 'rxjs/operators';
import { Observable } from 'rxjs/Observable';
describe('Angular async helper', () => {
let actuallyDone = false;
@@ -35,8 +34,8 @@ describe('Angular async helper', () => {
// Use done. Cannot use setInterval with async or fakeAsync
// See https://github.com/angular/angular/issues/10127
it('should run async test with successful delayed Observable', (done: DoneFn) => {
const source = of(true).pipe(delay(10));
it('should run async test with successful delayed Observable', (done: any) => {
const source = Observable.of(true).delay(10);
source.subscribe(
val => actuallyDone = true,
err => fail(err),
@@ -47,7 +46,7 @@ describe('Angular async helper', () => {
// Cannot use setInterval from within an async zone test
// See https://github.com/angular/angular/issues/10127
// xit('should run async test with successful delayed Observable', async(() => {
// const source = of(true).pipe(delay(10));
// const source = Observable.of(true).delay(10);
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)
@@ -57,7 +56,7 @@ describe('Angular async helper', () => {
// // Fail message: Error: 1 periodic timer(s) still in the queue
// // See https://github.com/angular/angular/issues/10127
// xit('should run async test with successful delayed Observable', fakeAsync(() => {
// const source = of(true).pipe(delay(10));
// const source = Observable.of(true).delay(10);
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)
@@ -1,5 +1,5 @@
// main app entry point
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { DemoModule } from './demo';
import { BagModule } from './bag';
platformBrowserDynamic().bootstrapModule(DemoModule);
platformBrowserDynamic().bootstrapModule(BagModule);
@@ -0,0 +1,130 @@
// #docplaster
import { DependentService, FancyService } from './bag';
///////// Fakes /////////
export class FakeFancyService extends FancyService {
value = 'faked value';
}
////////////////////////
// #docregion FancyService
// Straight Jasmine - no imports from Angular test libraries
describe('FancyService without the TestBed', () => {
let service: FancyService;
beforeEach(() => { service = new FancyService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getAsyncValue should return async value', (done: DoneFn) => {
service.getAsyncValue().then(value => {
expect(value).toBe('async value');
done();
});
});
// #docregion getTimeoutValue
it('#getTimeoutValue should return timeout value', (done: DoneFn) => {
service = new FancyService();
service.getTimeoutValue().then(value => {
expect(value).toBe('timeout value');
done();
});
});
// #enddocregion getTimeoutValue
it('#getObservableValue should return observable value', (done: DoneFn) => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
});
// #enddocregion FancyService
// DependentService requires injection of a FancyService
// #docregion DependentService
describe('DependentService without the TestBed', () => {
let service: DependentService;
it('#getValue should return real value by way of the real FancyService', () => {
service = new DependentService(new FancyService());
expect(service.getValue()).toBe('real value');
});
it('#getValue should return faked value by way of a fakeService', () => {
service = new DependentService(new FakeFancyService());
expect(service.getValue()).toBe('faked value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
service = new DependentService(fake as FancyService);
expect(service.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a FancyService spy', () => {
const fancy = new FancyService();
const stubValue = 'stub value';
const spy = spyOn(fancy, 'getValue').and.returnValue(stubValue);
service = new DependentService(fancy);
expect(service.getValue()).toBe(stubValue, 'service returned stub value');
expect(spy.calls.count()).toBe(1, 'stubbed method was called once');
expect(spy.calls.mostRecent().returnValue).toBe(stubValue);
});
});
// #enddocregion DependentService
// #docregion ReversePipe
import { ReversePipe } from './bag';
describe('ReversePipe', () => {
let pipe: ReversePipe;
beforeEach(() => { pipe = new ReversePipe(); });
it('transforms "abc" to "cba"', () => {
expect(pipe.transform('abc')).toBe('cba');
});
it('no change to palindrome: "able was I ere I saw elba"', () => {
const palindrome = 'able was I ere I saw elba';
expect(pipe.transform(palindrome)).toBe(palindrome);
});
});
// #enddocregion ReversePipe
import { ButtonComponent } from './bag';
// #docregion ButtonComp
describe('ButtonComp', () => {
let comp: ButtonComponent;
beforeEach(() => comp = new ButtonComponent());
it('#isOn should be false initially', () => {
expect(comp.isOn).toBe(false);
});
it('#clicked() should set #isOn to true', () => {
comp.clicked();
expect(comp.isOn).toBe(true);
});
it('#clicked() should set #message to "is on"', () => {
comp.clicked();
expect(comp.message).toMatch(/is on/i);
});
it('#clicked() should toggle #isOn', () => {
comp.clicked();
expect(comp.isOn).toBe(true);
comp.clicked();
expect(comp.isOn).toBe(false);
});
});
// #enddocregion ButtonComp
@@ -0,0 +1,681 @@
// #docplaster
import {
BagModule,
BankAccountComponent, BankAccountParentComponent,
ButtonComponent,
Child1Component, Child2Component, Child3Component,
FancyService,
ExternalTemplateComponent,
InputComponent,
IoComponent, IoParentComponent,
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
NeedsContentComponent, ParentComponent,
TestProvidersComponent, TestViewProvidersComponent,
ReversePipeComponent, ShellComponent
} from './bag';
import { By } from '@angular/platform-browser';
import { Component,
DebugElement,
Injectable } from '@angular/core';
import { FormsModule } from '@angular/forms';
// Forms symbols imported only for a specific test below
import { NgModel, NgControl } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { addMatchers, newEvent, click } from '../../testing';
beforeEach( addMatchers );
//////// Service Tests /////////////
// #docregion FancyService
describe('use inject helper in beforeEach', () => {
let service: FancyService;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [FancyService] });
// `TestBed.get` returns the injectable or an
// alternative object (including null) if the service provider is not found.
// Of course it will be found in this case because we're providing it.
// #docregion testbed-get
service = TestBed.get(FancyService, null);
// #enddocregion testbed-get
});
it('should use FancyService', () => {
expect(service.getValue()).toBe('real value');
});
it('should use FancyService', () => {
expect(service.getValue()).toBe('real value');
});
it('test should wait for FancyService.getAsyncValue', async(() => {
service.getAsyncValue().then(
value => expect(value).toBe('async value')
);
}));
it('test should wait for FancyService.getTimeoutValue', async(() => {
service.getTimeoutValue().then(
value => expect(value).toBe('timeout value')
);
}));
it('test should wait for FancyService.getObservableValue', async(() => {
service.getObservableValue().subscribe(
value => expect(value).toBe('observable value')
);
}));
// Must use done. See https://github.com/angular/angular/issues/10127
it('test should wait for FancyService.getObservableDelayValue', (done: DoneFn) => {
service.getObservableDelayValue().subscribe(value => {
expect(value).toBe('observable delay value');
done();
});
});
it('should allow the use of fakeAsync', fakeAsync(() => {
let value: any;
service.getAsyncValue().then((val: any) => value = val);
tick(); // Trigger JS engine cycle until all promises resolve.
expect(value).toBe('async value');
}));
});
// #enddocregion FancyService
describe('use inject within `it`', () => {
// #docregion getTimeoutValue
beforeEach(() => {
TestBed.configureTestingModule({ providers: [FancyService] });
});
// #enddocregion getTimeoutValue
it('should use modified providers',
inject([FancyService], (service: FancyService) => {
service.setValue('value modified in beforeEach');
expect(service.getValue()).toBe('value modified in beforeEach');
})
);
// #docregion getTimeoutValue
it('test should wait for FancyService.getTimeoutValue',
async(inject([FancyService], (service: FancyService) => {
service.getTimeoutValue().then(
value => expect(value).toBe('timeout value')
);
})));
// #enddocregion getTimeoutValue
});
describe('using async(inject) within beforeEach', () => {
let serviceValue: string;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [FancyService] });
});
beforeEach( async(inject([FancyService], (service: FancyService) => {
service.getAsyncValue().then(value => serviceValue = value);
})));
it('should use asynchronously modified value ... in synchronous test', () => {
expect(serviceValue).toBe('async value');
});
});
/////////// Component Tests //////////////////
describe('TestBed Component Tests', () => {
beforeEach( async(() => {
TestBed
.configureTestingModule({
imports: [BagModule],
})
// Compile everything in BagModule
.compileComponents();
}));
it('should create a component with inline template', () => {
const fixture = TestBed.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Child');
});
it('should create a component with external template', () => {
const fixture = TestBed.createComponent(ExternalTemplateComponent);
fixture.detectChanges();
expect(fixture).toHaveText('from external template');
});
it('should allow changing members of the component', () => {
const fixture = TestBed.createComponent(MyIfComponent);
fixture.detectChanges();
expect(fixture).toHaveText('MyIf()');
fixture.componentInstance.showMore = true;
fixture.detectChanges();
expect(fixture).toHaveText('MyIf(More)');
});
it('should create a nested component bound to inputs/outputs', () => {
const fixture = TestBed.createComponent(IoParentComponent);
fixture.detectChanges();
const heroes = fixture.debugElement.queryAll(By.css('.hero'));
expect(heroes.length).toBeGreaterThan(0, 'has heroes');
const comp = fixture.componentInstance;
const hero = comp.heroes[0];
click(heroes[0]);
fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('p'));
expect(selected).toHaveText(hero.name);
});
it('can access the instance variable of an `*ngFor` row component', () => {
const fixture = TestBed.createComponent(IoParentComponent);
const comp = fixture.componentInstance;
const heroName = comp.heroes[0].name; // first hero's name
fixture.detectChanges();
const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow
const hero = ngForRow.context['hero']; // the hero object passed into the row
expect(hero.name).toBe(heroName, 'ngRow.context.hero');
const rowComp = ngForRow.componentInstance;
// jasmine.any is an "instance-of-type" test.
expect(rowComp).toEqual(jasmine.any(IoComponent), 'component is IoComp');
expect(rowComp.hero.name).toBe(heroName, 'component.hero');
});
// #docregion ButtonComp
it('should support clicking a button', () => {
const fixture = TestBed.createComponent(ButtonComponent);
const btn = fixture.debugElement.query(By.css('button'));
const span = fixture.debugElement.query(By.css('span')).nativeElement;
fixture.detectChanges();
expect(span.textContent).toMatch(/is off/i, 'before click');
click(btn);
fixture.detectChanges();
expect(span.textContent).toMatch(/is on/i, 'after click');
});
// #enddocregion ButtonComp
// ngModel is async so we must wait for it with promise-based `whenStable`
it('should support entering text in input box (ngModel)', async(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
fixture.whenStable().then(() => {
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
return fixture.whenStable();
})
.then(() => {
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
});
}));
// fakeAsync version of ngModel input test enables sync test style
// synchronous `tick` replaces asynchronous promise-base `whenStable`
it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
tick();
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
tick();
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
}));
// #docregion ReversePipeComp
it('ReversePipeComp should reverse the input text', fakeAsync(() => {
const inputText = 'the quick brown fox.';
const expectedText = '.xof nworb kciuq eht';
const fixture = TestBed.createComponent(ReversePipeComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
// simulate user entering new name in input
input.value = inputText;
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.text
// and Angular updates the output span
input.dispatchEvent(newEvent('input'));
tick();
fixture.detectChanges();
expect(span.textContent).toBe(expectedText, 'output span');
expect(comp.text).toBe(inputText, 'component.text');
}));
// #enddocregion ReversePipeComp
// Use this technique to find attached directives of any kind
it('can examine attached directives and listeners', () => {
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'));
expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive');
const ngControl = inputEl.injector.get(NgControl);
expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive');
expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached');
});
// #docregion dom-attributes
it('BankAccountComponent should set attributes, styles, classes, and properties', () => {
const fixture = TestBed.createComponent(BankAccountParentComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
// the only child is debugElement of the BankAccount component
const el = fixture.debugElement.children[0];
const childComp = el.componentInstance as BankAccountComponent;
expect(childComp).toEqual(jasmine.any(BankAccountComponent));
expect(el.context).toBe(childComp, 'context is the child component');
expect(el.attributes['account']).toBe(childComp.id, 'account attribute');
expect(el.attributes['bank']).toBe(childComp.bank, 'bank attribute');
expect(el.classes['closed']).toBe(true, 'closed class');
expect(el.classes['open']).toBe(false, 'open class');
expect(el.styles['color']).toBe(comp.color, 'color style');
expect(el.styles['width']).toBe(comp.width + 'px', 'width style');
// #enddocregion dom-attributes
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
// #docregion dom-attributes
});
// #enddocregion dom-attributes
});
describe('TestBed Component Overrides:', () => {
it('should override ChildComp\'s template', () => {
const fixture = TestBed.configureTestingModule({
declarations: [Child1Component],
})
.overrideComponent(Child1Component, {
set: { template: '<span>Fake</span>' }
})
.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Fake');
});
it('should override TestProvidersComp\'s FancyService provider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestProvidersComponent],
})
.overrideComponent(TestProvidersComponent, {
remove: { providers: [FancyService]},
add: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
// Or replace them all (this component has only one provider)
// set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] },
})
.createComponent(TestProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value', 'text');
// Explore the providerTokens
const tokens = fixture.debugElement.providerTokens;
expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor');
expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp');
expect(tokens).toContain(FancyService, 'FancyService');
});
it('should override TestViewProvidersComp\'s FancyService viewProvider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestViewProvidersComponent],
})
.overrideComponent(TestViewProvidersComponent, {
// remove: { viewProviders: [FancyService]},
// add: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
// Or replace them all (this component has only one viewProvider)
set: { viewProviders: [{ provide: FancyService, useClass: FakeFancyService }] },
})
.createComponent(TestViewProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value');
});
it('injected provider should not be same as component\'s provider', () => {
// TestComponent is parent of TestProvidersComponent
@Component({ template: '<my-service-comp></my-service-comp>' })
class TestComponent {}
// 3 levels of FancyService provider: module, TestCompomponent, TestProvidersComponent
const fixture = TestBed.configureTestingModule({
declarations: [TestComponent, TestProvidersComponent],
providers: [FancyService]
})
.overrideComponent(TestComponent, {
set: { providers: [{ provide: FancyService, useValue: {} }] }
})
.overrideComponent(TestProvidersComponent, {
set: { providers: [{ provide: FancyService, useClass: FakeFancyService }] }
})
.createComponent(TestComponent);
let testBedProvider: FancyService;
let tcProvider: {};
let tpcProvider: FakeFancyService;
// `inject` uses TestBed's injector
inject([FancyService], (s: FancyService) => testBedProvider = s)();
tcProvider = fixture.debugElement.injector.get(FancyService);
tpcProvider = fixture.debugElement.children[0].injector.get(FancyService) as FakeFancyService;
expect(testBedProvider).not.toBe(<any> tcProvider, 'testBed/tc not same providers');
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
expect(testBedProvider instanceof FancyService).toBe(true, 'testBedProvider is FancyService');
expect(tcProvider).toEqual({}, 'tcProvider is {}');
expect(tpcProvider instanceof FakeFancyService).toBe(true, 'tpcProvider is FakeFancyService');
});
it('can access template local variables as references', () => {
const fixture = TestBed.configureTestingModule({
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
})
.overrideComponent(ShellComponent, {
set: {
selector: 'test-shell',
template: `
<needs-content #nc>
<child-1 #content text="My"></child-1>
<child-2 #content text="dog"></child-2>
<child-2 text="has"></child-2>
<child-3 #content text="fleas"></child-3>
<div #content>!</div>
</needs-content>
`
}
})
.createComponent(ShellComponent);
fixture.detectChanges();
// NeedsContentComp is the child of ShellComp
const el = fixture.debugElement.children[0];
const comp = el.componentInstance;
expect(comp.children.toArray().length).toBe(4,
'three different child components and an ElementRef with #content');
expect(el.references['nc']).toBe(comp, '#nc reference to component');
// #docregion custom-predicate
// Filter for DebugElements with a #content reference
const contentRefs = el.queryAll( de => de.references['content']);
// #enddocregion custom-predicate
expect(contentRefs.length).toBe(4, 'elements w/ a #content reference');
});
});
describe('Nested (one-deep) component override', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildComponent]
})
.compileComponents();
}));
it('ParentComp should use Fake Child component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child)');
});
});
describe('Nested (two-deep) component override', () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
})
.compileComponents();
}));
it('should use Fake Grandchild component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))');
});
});
describe('Lifecycle hooks w/ MyIfParentComp', () => {
let fixture: ComponentFixture<MyIfParentComponent>;
let parent: MyIfParentComponent;
let child: MyIfChildComponent;
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [MyIfChildComponent, MyIfParentComponent]
})
.compileComponents().then(() => {
fixture = TestBed.createComponent(MyIfParentComponent);
parent = fixture.componentInstance;
});
}));
it('should instantiate parent component', () => {
expect(parent).not.toBeNull('parent component should exist');
});
it('parent component OnInit should NOT be called before first detectChanges()', () => {
expect(parent.ngOnInitCalled).toBe(false);
});
it('parent component OnInit should be called after first detectChanges()', () => {
fixture.detectChanges();
expect(parent.ngOnInitCalled).toBe(true);
});
it('child component should exist after OnInit', () => {
fixture.detectChanges();
getChild();
expect(child instanceof MyIfChildComponent).toBe(true, 'should create child');
});
it('should have called child component\'s OnInit ', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnInitCalled).toBe(true);
});
it('child component called OnChanges once', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnChangesCounter).toBe(1);
});
it('changed parent value flows to child', () => {
fixture.detectChanges();
getChild();
parent.parentValue = 'foo';
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(child.childValue).toBe('foo',
'childValue should eq changed parent value');
});
// must be async test to see child flow to parent
it('changed child value flows to parent', async(() => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
return new Promise(resolve => {
// Wait one JS engine turn!
setTimeout(() => resolve(), 0);
})
.then(() => {
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(parent.parentValue).toBe('bar',
'parentValue should eq changed parent value');
});
}));
it('clicking "Close Child" triggers child OnDestroy', () => {
fixture.detectChanges();
getChild();
const btn = fixture.debugElement.query(By.css('button'));
click(btn);
fixture.detectChanges();
expect(child.ngOnDestroyCalled).toBe(true);
});
////// helpers ///
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
// The Hard Way: requires detailed knowledge of the parent template
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances:
childDe = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0];
// WE'LL USE THIS APPROACH !
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(function (de) { return de.componentInstance instanceof MyIfChildComponent; });
if (childDe && childDe.componentInstance) {
child = childDe.componentInstance;
} else {
fail('Unable to find MyIfChildComp within MyIfParentComp');
}
return child;
}
});
////////// Fakes ///////////
@Component({
selector: 'child-1',
template: `Fake Child`
})
class FakeChildComponent { }
@Component({
selector: 'child-1',
template: `Fake Child(<grandchild-1></grandchild-1>)`
})
class FakeChildWithGrandchildComponent { }
@Component({
selector: 'grandchild-1',
template: `Fake Grandchild`
})
class FakeGrandchildComponent { }
@Injectable()
class FakeFancyService extends FancyService {
value = 'faked value';
}
@@ -6,8 +6,9 @@ import { Component, ContentChildren, Directive, EventEmitter,
Pipe, PipeTransform,
SimpleChange } from '@angular/core';
import { of } from 'rxjs/observable/of';
import { delay } from 'rxjs/operators';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/delay';
////////// The App: Services and Components for the tests. //////////////
@@ -16,31 +17,37 @@ export class Hero {
}
////////// Services ///////////////
// #docregion ValueService
// #docregion FancyService
@Injectable()
export class ValueService {
export class FancyService {
protected value = 'real value';
getValue() { return this.value; }
setValue(value: string) { this.value = value; }
getObservableValue() { return of('observable value'); }
getAsyncValue() { return Promise.resolve('async value'); }
getPromiseValue() { return Promise.resolve('promise value'); }
getObservableValue() { return Observable.of('observable value'); }
getTimeoutValue() {
return new Promise((resolve) => {
setTimeout(() => { resolve('timeout value'); }, 10);
});
}
getObservableDelayValue() {
return of('observable delay value').pipe(delay(10));
return Observable.of('observable delay value').delay(10);
}
}
// #enddocregion ValueService
// #enddocregion FancyService
// #docregion MasterService
// #docregion DependentService
@Injectable()
export class MasterService {
constructor(private masterService: ValueService) { }
getValue() { return this.masterService.getValue(); }
export class DependentService {
constructor(private dependentService: FancyService) { }
getValue() { return this.dependentService.getValue(); }
}
// #enddocregion MasterService
// #enddocregion DependentService
/////////// Pipe ////////////////
/*
@@ -95,19 +102,19 @@ export class BankAccountParentComponent {
isClosed = true;
}
// #docregion LightswitchComp
// #docregion ButtonComp
@Component({
selector: 'lightswitch-comp',
selector: 'button-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class LightswitchComponent {
export class ButtonComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
// #enddocregion LightswitchComp
// #enddocregion ButtonComp
@Component({
selector: 'child-1',
@@ -224,31 +231,31 @@ export class MyIfComponent {
@Component({
selector: 'my-service-comp',
template: `injected value: {{valueService.value}}`,
providers: [ValueService]
template: `injected value: {{fancyService.value}}`,
providers: [FancyService]
})
export class TestProvidersComponent {
constructor(public valueService: ValueService) {}
constructor(public fancyService: FancyService) {}
}
@Component({
selector: 'my-service-comp',
template: `injected value: {{valueService.value}}`,
viewProviders: [ValueService]
template: `injected value: {{fancyService.value}}`,
viewProviders: [FancyService]
})
export class TestViewProvidersComponent {
constructor(public valueService: ValueService) {}
constructor(public fancyService: FancyService) {}
}
@Component({
selector: 'external-template-comp',
templateUrl: './demo-external-template.html'
templateUrl: './bag-external-template.html'
})
export class ExternalTemplateComponent implements OnInit {
serviceValue: string;
constructor(@Optional() private service: ValueService) { }
constructor(@Optional() private service: FancyService) { }
ngOnInit() {
if (this.service) { this.serviceValue = this.service.getValue(); }
@@ -369,9 +376,9 @@ export class ReversePipeComponent {
export class ShellComponent { }
@Component({
selector: 'demo-comp',
selector: 'bag-comp',
template: `
<h1>Specs Demo</h1>
<h1>Specs Bag</h1>
<my-if-parent-comp></my-if-parent-comp>
<hr>
<h3>Input/Output Component</h3>
@@ -390,7 +397,7 @@ export class ShellComponent { }
<input-value-comp></input-value-comp>
<hr>
<h3>Button Component</h3>
<lightswitch-comp></lightswitch-comp>
<button-comp></button-comp>
<hr>
<h3>Needs Content</h3>
<needs-content #nc>
@@ -402,13 +409,13 @@ export class ShellComponent { }
</needs-content>
`
})
export class DemoComponent { }
export class BagComponent { }
//////// Aggregations ////////////
export const demoDeclarations = [
DemoComponent,
export const bagDeclarations = [
BagComponent,
BankAccountComponent, BankAccountParentComponent,
LightswitchComponent,
ButtonComponent,
Child1Component, Child2Component, Child3Component,
ExternalTemplateComponent, InnerCompWithExternalTemplateComponent,
InputComponent,
@@ -420,7 +427,7 @@ export const demoDeclarations = [
ReversePipe, ReversePipeComponent, ShellComponent
];
export const demoProviders = [MasterService, ValueService];
export const bagProviders = [DependentService, FancyService];
////////////////////
////////////
@@ -430,10 +437,10 @@ import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: demoDeclarations,
providers: demoProviders,
entryComponents: [DemoComponent],
bootstrap: [DemoComponent]
declarations: bagDeclarations,
providers: bagProviders,
entryComponents: [BagComponent],
bootstrap: [BagComponent]
})
export class DemoModule { }
export class BagModule { }
@@ -0,0 +1,55 @@
// #docplaster
// #docregion imports
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-inline.component';
// #enddocregion imports
// #docregion setup
describe('BannerComponent (inline template)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
// #docregion before-each
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
});
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #enddocregion before-each
// #enddocregion setup
// #docregion test-w-o-detect-changes
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
// #enddocregion test-w-o-detect-changes
// #docregion tests
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
// #enddocregion tests
// #docregion setup
});
// #enddocregion setup
@@ -1,14 +1,11 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
// #docregion metadata
@Component({
selector: 'app-banner',
templateUrl: './banner-external.component.html',
styleUrls: ['./banner-external.component.css']
template: '<h1>{{title}}</h1>'
})
// #enddocregion metadata
export class BannerComponent {
title = 'Test Tour of Heroes';
}
@@ -7,45 +7,53 @@ import { async } from '@angular/core/testing';
import { ComponentFixtureAutoDetect } from '@angular/core/testing';
// #enddocregion import-ComponentFixtureAutoDetect
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (AutoChangeDetect)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
let de: DebugElement;
let el: HTMLElement;
beforeEach(() => {
beforeEach(async(() => {
// #docregion auto-detect
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
});
})
// #enddocregion auto-detect
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #docregion auto-detect-tests
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(h1.textContent).toContain(comp.title);
expect(el.textContent).toContain(comp.title);
});
it('should still see original title after comp.title change', () => {
const oldTitle = comp.title;
comp.title = 'Test Title';
// Displayed title is old because Angular didn't hear the change :(
expect(h1.textContent).toContain(oldTitle);
expect(el.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(h1.textContent).toContain(comp.title);
expect(el.textContent).toContain(comp.title);
});
// #enddocregion auto-detect-tests
});
@@ -0,0 +1,53 @@
// #docplaster
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (templateUrl)', () => {
let comp: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let de: DebugElement;
let el: HTMLElement;
// #docregion async-before-each
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ], // declare the test component
})
.compileComponents(); // compile template and css
}));
// #enddocregion async-before-each
// #docregion sync-before-each
// synchronous beforeEach
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance; // BannerComponent test instance
// query for the title <h1> by CSS element selector
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
});
// #enddocregion sync-before-each
it('no title in the DOM until manually call `detectChanges`', () => {
expect(el.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.title);
});
it('should display a different test title', () => {
comp.title = 'Test Title';
fixture.detectChanges();
expect(el.textContent).toContain('Test Title');
});
});
@@ -1,12 +1,12 @@
// #docregion
import { Component } from '@angular/core';
// #docregion component
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>',
styles: ['h1 { color: green; font-size: 350%}']
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.css']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
// #enddocregion component
@@ -1,72 +0,0 @@
// #docplaster
// #docregion import-async
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
// #enddocregion import-async
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner-external.component';
describe('BannerComponent (external files)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
describe('Two beforeEach', () => {
// #docregion async-before-each
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
})
.compileComponents(); // compile template and css
}));
// #enddocregion async-before-each
// synchronous beforeEach
// #docregion sync-before-each
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance; // BannerComponent test instance
h1 = fixture.nativeElement.querySelector('h1');
});
// #enddocregion sync-before-each
tests();
});
describe('One beforeEach', () => {
// #docregion one-before-each
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
})
.compileComponents()
.then(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
h1 = fixture.nativeElement.querySelector('h1');
});
}));
// #enddocregion one-before-each
tests();
});
function tests() {
it('no title in the DOM until manually call `detectChanges`', () => {
expect(h1.textContent).toEqual('');
});
it('should display original title', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain('Test Title');
});
}
});
@@ -1,119 +0,0 @@
// #docplaster
// #docregion import-by
import { By } from '@angular/platform-browser';
// #enddocregion import-by
// #docregion import-debug-element
import { DebugElement } from '@angular/core';
// #enddocregion import-debug-element
// #docregion v1
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
// #enddocregion v1
import { BannerComponent } from './banner-initial.component';
/*
// #docregion v1
import { BannerComponent } from './banner.component';
describe('BannerComponent', () => {
// #enddocregion v1
*/
describe('BannerComponent (initial CLI generated)', () => {
// #docregion v1
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeDefined();
});
});
// #enddocregion v1
// #docregion v2
describe('BannerComponent (minimal)', () => {
it('should create', () => {
// #docregion configureTestingModule
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
});
// #enddocregion configureTestingModule
// #docregion createComponent
const fixture = TestBed.createComponent(BannerComponent);
// #enddocregion createComponent
// #docregion componentInstance
const component = fixture.componentInstance;
expect(component).toBeDefined();
// #enddocregion componentInstance
});
});
// #enddocregion v2
// #docregion v3, v4
describe('BannerComponent (with beforeEach)', () => {
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ]
});
fixture = TestBed.createComponent(BannerComponent);
component = fixture.componentInstance;
});
it('should create', () => {
expect(component).toBeDefined();
});
// #enddocregion v3
// #docregion v4-test-2
it('should contain "banner works!"', () => {
const bannerElement: HTMLElement = fixture.nativeElement;
expect(bannerElement.textContent).toContain('banner works!');
});
// #enddocregion v4-test-2
// #docregion v4-test-3
it('should have <p> with "banner works!"', () => {
// #docregion nativeElement
const bannerElement: HTMLElement = fixture.nativeElement;
// #enddocregion nativeElement
const p = bannerElement.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
// #enddocregion v4-test-3
// #docregion v4-test-4
it('should find the <p> with fixture.debugElement.nativeElement)', () => {
// #docregion debugElement-nativeElement
const bannerDe: DebugElement = fixture.debugElement;
const bannerEl: HTMLElement = bannerDe.nativeElement;
// #enddocregion debugElement-nativeElement
const p = bannerEl.querySelector('p');
expect(p.textContent).toEqual('banner works!');
});
// #enddocregion v4-test-4
// #docregion v4-test-5
it('should find the <p> with fixture.debugElement.query(By.css)', () => {
const bannerDe: DebugElement = fixture.debugElement;
const paragraphDe = bannerDe.query(By.css('p'));
const p: HTMLElement = paragraphDe.nativeElement;
expect(p.textContent).toEqual('banner works!');
});
// #enddocregion v4-test-5
// #docregion v3
});
// #enddocregion v3, v4
@@ -1,10 +0,0 @@
// BannerComponent as initially generated by the CLI
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'app-banner',
template: `<p>banner works!</p>`,
styles: []
})
export class BannerComponent { }
@@ -1,56 +0,0 @@
// #docplaster
// #docregion
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { BannerComponent } from './banner.component';
describe('BannerComponent (inline template)', () => {
// #docregion setup
let component: BannerComponent;
let fixture: ComponentFixture<BannerComponent>;
let h1: HTMLElement;
// #docregion configure-and-create
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
});
fixture = TestBed.createComponent(BannerComponent);
// #enddocregion configure-and-create
component = fixture.componentInstance; // BannerComponent test instance
h1 = fixture.nativeElement.querySelector('h1');
// #docregion configure-and-create
});
// #enddocregion setup, configure-and-create
// #docregion test-w-o-detect-changes
it('no title in the DOM after createComponent()', () => {
expect(h1.textContent).toEqual('');
});
// #enddocregion test-w-o-detect-changes
// #docregion expect-h1-default-v1
it('should display original title', () => {
// #enddocregion expect-h1-default-v1
fixture.detectChanges();
// #docregion expect-h1-default-v1
expect(h1.textContent).toContain(component.title);
});
// #enddocregion expect-h1-default-v1
// #docregion expect-h1-default
it('should display original title after detectChanges()', () => {
fixture.detectChanges();
expect(h1.textContent).toContain(component.title);
});
// #enddocregion expect-h1-default
// #docregion after-change
it('should display a different test title', () => {
component.title = 'Test Title';
fixture.detectChanges();
expect(h1.textContent).toContain('Test Title');
});
// #enddocregion after-change
});
@@ -0,0 +1,4 @@
<!-- #docregion -->
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>
@@ -1,9 +1,7 @@
// #docplaster
import { async, ComponentFixture, TestBed
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { addMatchers, click } from '../../testing';
@@ -13,96 +11,64 @@ import { DashboardHeroComponent } from './dashboard-hero.component';
beforeEach( addMatchers );
describe('DashboardHeroComponent class only', () => {
// #docregion class-only
it('raises the selected event when clicked', () => {
const comp = new DashboardHeroComponent();
const hero: Hero = { id: 42, name: 'Test' };
comp.hero = hero;
comp.selected.subscribe(selectedHero => expect(selectedHero).toBe(hero));
comp.click();
});
// #enddocregion class-only
});
describe('DashboardHeroComponent when tested directly', () => {
let comp: DashboardHeroComponent;
let expectedHero: Hero;
let fixture: ComponentFixture<DashboardHeroComponent>;
let heroDe: DebugElement;
let heroEl: HTMLElement;
let heroEl: DebugElement;
beforeEach(async(() => {
// #docregion setup, config-testbed
// #docregion setup, compile-components
// async beforeEach
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ]
declarations: [ DashboardHeroComponent ],
})
// #enddocregion setup, config-testbed
.compileComponents();
.compileComponents(); // compile template and css
}));
// #enddocregion compile-components
// synchronous beforeEach
beforeEach(() => {
// #docregion setup
fixture = TestBed.createComponent(DashboardHeroComponent);
comp = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero element
// find the hero's DebugElement and element
heroDe = fixture.debugElement.query(By.css('.hero'));
heroEl = heroDe.nativeElement;
// mock the hero supplied by the parent component
expectedHero = { id: 42, name: 'Test Name' };
// simulate the parent setting the input property with that hero
// pretend that it was wired to something that supplied a hero
expectedHero = new Hero(42, 'Test Name');
comp.hero = expectedHero;
// trigger initial data binding
fixture.detectChanges();
// #enddocregion setup
fixture.detectChanges(); // trigger initial data binding
});
// #enddocregion setup
// #docregion name-test
it('should display hero name in uppercase', () => {
it('should display hero name', () => {
const expectedPipedName = expectedHero.name.toUpperCase();
expect(heroEl.textContent).toContain(expectedPipedName);
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
});
// #enddocregion name-test
// #docregion click-test
it('should raise selected event when clicked (triggerEventHandler)', () => {
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
// #docregion trigger-event-handler
heroDe.triggerEventHandler('click', null);
heroEl.triggerEventHandler('click', null);
// #enddocregion trigger-event-handler
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test
// #docregion click-test-2
it('should raise selected event when clicked (element.click)', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
heroEl.click();
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test-2
// #docregion click-test-3
it('should raise selected event when clicked (click helper)', () => {
// #docregion click-test-2
it('should raise selected event when clicked', () => {
let selectedHero: Hero;
comp.selected.subscribe(hero => selectedHero = hero);
click(heroDe); // click helper with DebugElement
click(heroEl); // click helper with native element
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
click(heroEl); // triggerEventHandler helper
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test-3
// #enddocregion click-test-2
});
//////////////////
@@ -110,31 +76,28 @@ describe('DashboardHeroComponent when tested directly', () => {
describe('DashboardHeroComponent when inside a test host', () => {
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let heroEl: HTMLElement;
let heroEl: DebugElement;
beforeEach(async(() => {
// #docregion test-host-setup
// #docregion test-host-setup
beforeEach( async(() => {
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent, TestHostComponent ]
})
// #enddocregion test-host-setup
.compileComponents();
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
}).compileComponents();
}));
beforeEach(() => {
// #docregion test-host-setup
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.nativeElement.querySelector('.hero');
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
fixture.detectChanges(); // trigger initial data binding
// #enddocregion test-host-setup
});
// #enddocregion test-host-setup
// #docregion test-host-tests
it('should display hero name', () => {
const expectedPipedName = testHost.hero.name.toUpperCase();
expect(heroEl.textContent).toContain(expectedPipedName);
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked', () => {
@@ -151,12 +114,10 @@ import { Component } from '@angular/core';
// #docregion test-host
@Component({
template: `
<dashboard-hero
[hero]="hero" (selected)="onSelected($event)">
</dashboard-hero>`
<dashboard-hero [hero]="hero" (selected)="onSelected($event)"></dashboard-hero>`
})
class TestHostComponent {
hero: Hero = {id: 42, name: 'Test Name' };
hero = new Hero(42, 'Test Name');
selectedHero: Hero;
onSelected(hero: Hero) { this.selectedHero = hero; }
}
@@ -5,17 +5,13 @@ import { Hero } from '../model/hero';
// #docregion component
@Component({
selector: 'dashboard-hero',
template: `
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>`,
selector: 'dashboard-hero',
templateUrl: './dashboard-hero.component.html',
styleUrls: [ './dashboard-hero.component.css' ]
})
// #docregion class
export class DashboardHeroComponent {
@Input() hero: Hero;
@Output() selected = new EventEmitter<Hero>();
click() { this.selected.emit(this.hero); }
}
// #enddocregion component, class
// #enddocregion component
@@ -1,24 +1,24 @@
import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { Hero } from '../model/hero';
import { Hero } from '../model';
import { addMatchers } from '../../testing';
import { TestHeroService, HeroService } from '../model/testing/test-hero.service';
import { FakeHeroService } from '../model/testing';
class FakeRouter {
navigateByUrl(url: string) { return url; }
}
describe('DashboardComponent class only', () => {
describe('DashboardComponent: w/o Angular TestBed', () => {
let comp: DashboardComponent;
let heroService: TestHeroService;
let heroService: FakeHeroService;
let router: Router;
beforeEach(() => {
addMatchers();
router = new FakeRouter() as any as Router;
heroService = new TestHeroService();
heroService = new FakeHeroService();
comp = new DashboardComponent(router, heroService);
});
@@ -35,19 +35,17 @@ describe('DashboardComponent class only', () => {
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
comp.ngOnInit(); // ngOnInit -> getHeroes
heroService.lastResult // the one from getHeroes
.subscribe(
() => {
heroService.lastPromise // the one from getHeroes
.then(() => {
// throw new Error('deliberate error'); // see it fail gracefully
expect(comp.heroes.length).toBeGreaterThan(0,
'should have heroes after service promise resolves');
done();
},
done.fail);
})
.then(done, done.fail);
});
it('should tell ROUTER to navigate by hero id', () => {
const hero: Hero = {id: 42, name: 'Abbracadabra' };
const hero = new Hero(42, 'Abbracadabra');
const spy = spyOn(router, 'navigateByUrl');
comp.gotoDetail(hero);
@@ -2,9 +2,9 @@
import { async, inject, ComponentFixture, TestBed
} from '@angular/core/testing';
import { addMatchers, asyncData, click } from '../../testing';
import { HeroService } from '../model/hero.service';
import { getTestHeroes } from '../model/testing/test-heroes';
import { addMatchers, click } from '../../testing';
import { HeroService } from '../model';
import { FakeHeroService } from '../model/testing';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
@@ -12,6 +12,12 @@ import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { DashboardModule } from './dashboard.module';
// #docregion router-stub
class RouterStub {
navigateByUrl(url: string) { return url; }
}
// #enddocregion router-stub
beforeEach ( addMatchers );
let comp: DashboardComponent;
@@ -31,8 +37,8 @@ describe('DashboardComponent (deep)', () => {
tests(clickForDeep);
function clickForDeep() {
// get first <div class="hero">
const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero');
// get first <div class="hero"> DebugElement
const heroEl = fixture.debugElement.query(By.css('.hero'));
click(heroEl);
}
});
@@ -55,32 +61,24 @@ describe('DashboardComponent (shallow)', () => {
function clickForShallow() {
// get first <dashboard-hero> DebugElement
const heroDe = fixture.debugElement.query(By.css('dashboard-hero'));
heroDe.triggerEventHandler('selected', comp.heroes[0]);
const heroEl = fixture.debugElement.query(By.css('dashboard-hero'));
heroEl.triggerEventHandler('selected', comp.heroes[0]);
}
});
/** Add TestBed providers, compile, and create DashboardComponent */
function compileAndCreate() {
// #docregion compile-and-create-body
beforeEach(async(() => {
// #docregion router-spy
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);
beforeEach( async(() => {
TestBed.configureTestingModule({
providers: [
{ provide: HeroService, useValue: heroServiceSpy },
{ provide: Router, useValue: routerSpy }
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub }
]
})
// #enddocregion router-spy
.compileComponents().then(() => {
fixture = TestBed.createComponent(DashboardComponent);
comp = fixture.componentInstance;
// getHeroes spy returns observable of test heroes
heroServiceSpy.getHeroes.and.returnValue(asyncData(getTestHeroes()));
});
// #enddocregion compile-and-create-body
}));
@@ -106,11 +104,8 @@ function tests(heroClick: Function) {
describe('after get dashboard heroes', () => {
let router: Router;
// Trigger component so it gets heroes and binds to them
beforeEach(async(() => {
router = fixture.debugElement.injector.get(Router);
beforeEach( async(() => {
fixture.detectChanges(); // runs ngOnInit -> getHeroes
fixture.whenStable() // No need for the `lastPromise` hack!
.then(() => fixture.detectChanges()); // bind to heroes
@@ -124,25 +119,29 @@ function tests(heroClick: Function) {
it('should DISPLAY heroes', () => {
// Find and examine the displayed heroes
// Look for them in the DOM by css class
const heroes = fixture.nativeElement.querySelectorAll('dashboard-hero');
const heroes = fixture.debugElement.queryAll(By.css('dashboard-hero'));
expect(heroes.length).toBe(4, 'should display 4 heroes');
});
// #docregion navigate-test
it('should tell ROUTER to navigate when hero clicked', () => {
// #docregion navigate-test, inject
it('should tell ROUTER to navigate when hero clicked',
inject([Router], (router: Router) => { // ...
// #enddocregion inject
const spy = spyOn(router, 'navigateByUrl');
heroClick(); // trigger click on first inner <div class="hero">
// args passed to router.navigateByUrl() spy
const spy = router.navigateByUrl as jasmine.Spy;
// args passed to router.navigateByUrl()
const navArgs = spy.calls.first().args[0];
// expecting to navigate to id of the component's first hero
const id = comp.heroes[0].id;
expect(navArgs).toBe('/heroes/' + id,
'should nav to HeroDetail for first hero');
});
// #enddocregion navigate-test
// #docregion inject
}));
// #enddocregion navigate-test, inject
});
}
@@ -2,7 +2,7 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Hero } from '../model/hero';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@Component({
@@ -23,7 +23,7 @@ export class DashboardComponent implements OnInit {
ngOnInit() {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
.then(heroes => this.heroes = heroes.slice(1, 5));
}
// #docregion goto-detail
@@ -1,153 +0,0 @@
// #docplaster
import {
LightswitchComponent,
MasterService,
ValueService,
ReversePipe
} from './demo';
///////// Fakes /////////
export class FakeValueService extends ValueService {
value = 'faked service value';
}
////////////////////////
describe('demo (no TestBed):', () => {
// #docregion ValueService
// Straight Jasmine testing without Angular's testing support
describe('ValueService', () => {
let service: ValueService;
beforeEach(() => { service = new ValueService(); });
it('#getValue should return real value', () => {
expect(service.getValue()).toBe('real value');
});
it('#getObservableValue should return value from observable',
(done: DoneFn) => {
service.getObservableValue().subscribe(value => {
expect(value).toBe('observable value');
done();
});
});
it('#getPromiseValue should return value from a promise',
(done: DoneFn) => {
service.getPromiseValue().then(value => {
expect(value).toBe('promise value');
done();
});
});
});
// #enddocregion ValueService
// MasterService requires injection of a ValueService
// #docregion MasterService
describe('MasterService without Angular testing support', () => {
let masterService: MasterService;
it('#getValue should return real value from the real service', () => {
masterService = new MasterService(new ValueService());
expect(masterService.getValue()).toBe('real value');
});
it('#getValue should return faked value from a fakeService', () => {
masterService = new MasterService(new FakeValueService());
expect(masterService.getValue()).toBe('faked service value');
});
it('#getValue should return faked value from a fake object', () => {
const fake = { getValue: () => 'fake value' };
masterService = new MasterService(fake as ValueService);
expect(masterService.getValue()).toBe('fake value');
});
it('#getValue should return stubbed value from a spy', () => {
// create `getValue` spy on an object representing the ValueService
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
// set the value to return when the `getValue` spy is called.
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
masterService = new MasterService(valueServiceSpy);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
});
// #enddocregion MasterService
describe('MasterService (no beforeEach)', () => {
// #docregion no-before-each-test
it('#getValue should return stubbed value from a spy', () => {
// #docregion no-before-each-setup-call
const { masterService, stubValue, valueServiceSpy } = setup();
// #enddocregion no-before-each-setup-call
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
// #enddocregion no-before-each-test
// #docregion no-before-each-setup
function setup() {
const valueServiceSpy =
jasmine.createSpyObj('ValueService', ['getValue']);
const stubValue = 'stub value';
const masterService = new MasterService(valueServiceSpy);
valueServiceSpy.getValue.and.returnValue(stubValue);
return { masterService, stubValue, valueServiceSpy };
}
// #enddocregion no-before-each-setup
});
// #docregion ReversePipe
describe('ReversePipe', () => {
let pipe: ReversePipe;
beforeEach(() => { pipe = new ReversePipe(); });
it('transforms "abc" to "cba"', () => {
expect(pipe.transform('abc')).toBe('cba');
});
it('no change to palindrome: "able was I ere I saw elba"', () => {
const palindrome = 'able was I ere I saw elba';
expect(pipe.transform(palindrome)).toBe(palindrome);
});
});
// #enddocregion ReversePipe
// #docregion Lightswitch
describe('LightswitchComp', () => {
it('#clicked() should toggle #isOn', () => {
const comp = new LightswitchComponent();
expect(comp.isOn).toBe(false, 'off at first');
comp.clicked();
expect(comp.isOn).toBe(true, 'on after click');
comp.clicked();
expect(comp.isOn).toBe(false, 'off after second click');
});
it('#clicked() should set #message to "is on"', () => {
const comp = new LightswitchComponent();
expect(comp.message).toMatch(/is off/i, 'off at first');
comp.clicked();
expect(comp.message).toMatch(/is on/i, 'on after clicked');
});
});
// #enddocregion Lightswitch
});
@@ -1,706 +0,0 @@
// #docplaster
import {
DemoModule,
BankAccountComponent, BankAccountParentComponent,
LightswitchComponent,
Child1Component, Child2Component, Child3Component,
MasterService,
ValueService,
ExternalTemplateComponent,
InputComponent,
IoComponent, IoParentComponent,
MyIfComponent, MyIfChildComponent, MyIfParentComponent,
NeedsContentComponent, ParentComponent,
TestProvidersComponent, TestViewProvidersComponent,
ReversePipeComponent, ShellComponent
} from './demo';
import { By } from '@angular/platform-browser';
import { Component,
DebugElement,
Injectable } from '@angular/core';
import { FormsModule } from '@angular/forms';
// Forms symbols imported only for a specific test below
import { NgModel, NgControl } from '@angular/forms';
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { addMatchers, newEvent, click } from '../../testing';
export class NotProvided extends ValueService { /* example below */}
beforeEach( addMatchers );
describe('demo (with TestBed):', () => {
//////// Service Tests /////////////
// #docregion ValueService
describe('ValueService', () => {
// #docregion value-service-before-each
let service: ValueService;
// #docregion value-service-inject-before-each
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
// #enddocregion value-service-before-each
service = TestBed.get(ValueService);
// #docregion value-service-before-each
});
// #enddocregion value-service-before-each, value-service-inject-before-each
// #docregion value-service-inject-it
it('should use ValueService', () => {
service = TestBed.get(ValueService);
expect(service.getValue()).toBe('real value');
});
// #enddocregion value-service-inject-it
it('can inject a default value when service is not provided', () => {
// #docregion testbed-get-w-null
service = TestBed.get(NotProvided, null); // service is null
// #enddocregion testbed-get-w-null
});
it('test should wait for ValueService.getPromiseValue', async(() => {
service.getPromiseValue().then(
value => expect(value).toBe('promise value')
);
}));
it('test should wait for ValueService.getObservableValue', async(() => {
service.getObservableValue().subscribe(
value => expect(value).toBe('observable value')
);
}));
// Must use done. See https://github.com/angular/angular/issues/10127
it('test should wait for ValueService.getObservableDelayValue', (done: DoneFn) => {
service.getObservableDelayValue().subscribe(value => {
expect(value).toBe('observable delay value');
done();
});
});
it('should allow the use of fakeAsync', fakeAsync(() => {
let value: any;
service.getPromiseValue().then((val: any) => value = val);
tick(); // Trigger JS engine cycle until all promises resolve.
expect(value).toBe('promise value');
}));
});
// #enddocregion ValueService
describe('MasterService', () => {
// #docregion master-service-before-each
let masterService: MasterService;
let valueServiceSpy: jasmine.SpyObj<ValueService>;
beforeEach(() => {
const spy = jasmine.createSpyObj('ValueService', ['getValue']);
TestBed.configureTestingModule({
// Provide both the service-to-test and its (spy) dependency
providers: [
MasterService,
{ provide: ValueService, useValue: spy }
]
});
// Inject both the service-to-test and its (spy) dependency
masterService = TestBed.get(MasterService);
valueServiceSpy = TestBed.get(ValueService);
});
// #enddocregion master-service-before-each
// #docregion master-service-it
it('#getValue should return stubbed value from a spy', () => {
const stubValue = 'stub value';
valueServiceSpy.getValue.and.returnValue(stubValue);
expect(masterService.getValue())
.toBe(stubValue, 'service returned stub value');
expect(valueServiceSpy.getValue.calls.count())
.toBe(1, 'spy method was called once');
expect(valueServiceSpy.getValue.calls.mostRecent().returnValue)
.toBe(stubValue);
});
// #enddocregion master-service-it
});
describe('use inject within `it`', () => {
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
it('should use modified providers',
inject([ValueService], (service: ValueService) => {
service.setValue('value modified in beforeEach');
expect(service.getValue())
.toBe('value modified in beforeEach');
})
);
});
describe('using async(inject) within beforeEach', () => {
let serviceValue: string;
beforeEach(() => {
TestBed.configureTestingModule({ providers: [ValueService] });
});
beforeEach(async(inject([ValueService], (service: ValueService) => {
service.getPromiseValue().then(value => serviceValue = value);
})));
it('should use asynchronously modified value ... in synchronous test', () => {
expect(serviceValue).toBe('promise value');
});
});
/////////// Component Tests //////////////////
describe('TestBed component tests', () => {
beforeEach(async(() => {
TestBed
.configureTestingModule({
imports: [DemoModule],
})
// Compile everything in DemoModule
.compileComponents();
}));
it('should create a component with inline template', () => {
const fixture = TestBed.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Child');
});
it('should create a component with external template', () => {
const fixture = TestBed.createComponent(ExternalTemplateComponent);
fixture.detectChanges();
expect(fixture).toHaveText('from external template');
});
it('should allow changing members of the component', () => {
const fixture = TestBed.createComponent(MyIfComponent);
fixture.detectChanges();
expect(fixture).toHaveText('MyIf()');
fixture.componentInstance.showMore = true;
fixture.detectChanges();
expect(fixture).toHaveText('MyIf(More)');
});
it('should create a nested component bound to inputs/outputs', () => {
const fixture = TestBed.createComponent(IoParentComponent);
fixture.detectChanges();
const heroes = fixture.debugElement.queryAll(By.css('.hero'));
expect(heroes.length).toBeGreaterThan(0, 'has heroes');
const comp = fixture.componentInstance;
const hero = comp.heroes[0];
click(heroes[0]);
fixture.detectChanges();
const selected = fixture.debugElement.query(By.css('p'));
expect(selected).toHaveText(hero.name);
});
it('can access the instance variable of an `*ngFor` row component', () => {
const fixture = TestBed.createComponent(IoParentComponent);
const comp = fixture.componentInstance;
const heroName = comp.heroes[0].name; // first hero's name
fixture.detectChanges();
const ngForRow = fixture.debugElement.query(By.directive(IoComponent)); // first hero ngForRow
const hero = ngForRow.context['hero']; // the hero object passed into the row
expect(hero.name).toBe(heroName, 'ngRow.context.hero');
const rowComp = ngForRow.componentInstance;
// jasmine.any is an "instance-of-type" test.
expect(rowComp).toEqual(jasmine.any(IoComponent), 'component is IoComp');
expect(rowComp.hero.name).toBe(heroName, 'component.hero');
});
// #docregion ButtonComp
it('should support clicking a button', () => {
const fixture = TestBed.createComponent(LightswitchComponent);
const btn = fixture.debugElement.query(By.css('button'));
const span = fixture.debugElement.query(By.css('span')).nativeElement;
fixture.detectChanges();
expect(span.textContent).toMatch(/is off/i, 'before click');
click(btn);
fixture.detectChanges();
expect(span.textContent).toMatch(/is on/i, 'after click');
});
// #enddocregion ButtonComp
// ngModel is async so we must wait for it with promise-based `whenStable`
it('should support entering text in input box (ngModel)', async(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
fixture.whenStable().then(() => {
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
return fixture.whenStable();
})
.then(() => {
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
});
}));
// fakeAsync version of ngModel input test enables sync test style
// synchronous `tick` replaces asynchronous promise-base `whenStable`
it('should support entering text in input box (ngModel) - fakeAsync', fakeAsync(() => {
const expectedOrigName = 'John';
const expectedNewName = 'Sally';
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = <HTMLInputElement> fixture.debugElement.query(By.css('input')).nativeElement;
expect(comp.name).toBe(expectedOrigName,
`At start name should be ${expectedOrigName} `);
// wait until ngModel binds comp.name to input box
tick();
expect(input.value).toBe(expectedOrigName,
`After ngModel updates input box, input.value should be ${expectedOrigName} `);
// simulate user entering new name in input
input.value = expectedNewName;
// that change doesn't flow to the component immediately
expect(comp.name).toBe(expectedOrigName,
`comp.name should still be ${expectedOrigName} after value change, before binding happens`);
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.name
input.dispatchEvent(newEvent('input'));
tick();
expect(comp.name).toBe(expectedNewName,
`After ngModel updates the model, comp.name should be ${expectedNewName} `);
}));
// #docregion ReversePipeComp
it('ReversePipeComp should reverse the input text', fakeAsync(() => {
const inputText = 'the quick brown fox.';
const expectedText = '.xof nworb kciuq eht';
const fixture = TestBed.createComponent(ReversePipeComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
const input = fixture.debugElement.query(By.css('input')).nativeElement as HTMLInputElement;
const span = fixture.debugElement.query(By.css('span')).nativeElement as HTMLElement;
// simulate user entering new name in input
input.value = inputText;
// dispatch a DOM event so that Angular learns of input value change.
// then wait a tick while ngModel pushes input.box value to comp.text
// and Angular updates the output span
input.dispatchEvent(newEvent('input'));
tick();
fixture.detectChanges();
expect(span.textContent).toBe(expectedText, 'output span');
expect(comp.text).toBe(inputText, 'component.text');
}));
// #enddocregion ReversePipeComp
// Use this technique to find attached directives of any kind
it('can examine attached directives and listeners', () => {
const fixture = TestBed.createComponent(InputComponent);
fixture.detectChanges();
const inputEl = fixture.debugElement.query(By.css('input'));
expect(inputEl.providerTokens).toContain(NgModel, 'NgModel directive');
const ngControl = inputEl.injector.get(NgControl);
expect(ngControl).toEqual(jasmine.any(NgControl), 'NgControl directive');
expect(inputEl.listeners.length).toBeGreaterThan(2, 'several listeners attached');
});
// #docregion dom-attributes
it('BankAccountComponent should set attributes, styles, classes, and properties', () => {
const fixture = TestBed.createComponent(BankAccountParentComponent);
fixture.detectChanges();
const comp = fixture.componentInstance;
// the only child is debugElement of the BankAccount component
const el = fixture.debugElement.children[0];
const childComp = el.componentInstance as BankAccountComponent;
expect(childComp).toEqual(jasmine.any(BankAccountComponent));
expect(el.context).toBe(childComp, 'context is the child component');
expect(el.attributes['account']).toBe(childComp.id, 'account attribute');
expect(el.attributes['bank']).toBe(childComp.bank, 'bank attribute');
expect(el.classes['closed']).toBe(true, 'closed class');
expect(el.classes['open']).toBe(false, 'open class');
expect(el.styles['color']).toBe(comp.color, 'color style');
expect(el.styles['width']).toBe(comp.width + 'px', 'width style');
// #enddocregion dom-attributes
// Removed on 12/02/2016 when ceased public discussion of the `Renderer`. Revive in future?
// expect(el.properties['customProperty']).toBe(true, 'customProperty');
// #docregion dom-attributes
});
// #enddocregion dom-attributes
});
describe('TestBed component overrides:', () => {
it('should override ChildComp\'s template', () => {
const fixture = TestBed.configureTestingModule({
declarations: [Child1Component],
})
.overrideComponent(Child1Component, {
set: { template: '<span>Fake</span>' }
})
.createComponent(Child1Component);
fixture.detectChanges();
expect(fixture).toHaveText('Fake');
});
it('should override TestProvidersComp\'s ValueService provider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestProvidersComponent],
})
.overrideComponent(TestProvidersComponent, {
remove: { providers: [ValueService]},
add: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
// Or replace them all (this component has only one provider)
// set: { providers: [{ provide: ValueService, useClass: FakeValueService }] },
})
.createComponent(TestProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value', 'text');
// Explore the providerTokens
const tokens = fixture.debugElement.providerTokens;
expect(tokens).toContain(fixture.componentInstance.constructor, 'component ctor');
expect(tokens).toContain(TestProvidersComponent, 'TestProvidersComp');
expect(tokens).toContain(ValueService, 'ValueService');
});
it('should override TestViewProvidersComp\'s ValueService viewProvider', () => {
const fixture = TestBed.configureTestingModule({
declarations: [TestViewProvidersComponent],
})
.overrideComponent(TestViewProvidersComponent, {
// remove: { viewProviders: [ValueService]},
// add: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
// Or replace them all (this component has only one viewProvider)
set: { viewProviders: [{ provide: ValueService, useClass: FakeValueService }] },
})
.createComponent(TestViewProvidersComponent);
fixture.detectChanges();
expect(fixture).toHaveText('injected value: faked value');
});
it('injected provider should not be same as component\'s provider', () => {
// TestComponent is parent of TestProvidersComponent
@Component({ template: '<my-service-comp></my-service-comp>' })
class TestComponent {}
// 3 levels of ValueService provider: module, TestCompomponent, TestProvidersComponent
const fixture = TestBed.configureTestingModule({
declarations: [TestComponent, TestProvidersComponent],
providers: [ValueService]
})
.overrideComponent(TestComponent, {
set: { providers: [{ provide: ValueService, useValue: {} }] }
})
.overrideComponent(TestProvidersComponent, {
set: { providers: [{ provide: ValueService, useClass: FakeValueService }] }
})
.createComponent(TestComponent);
let testBedProvider: ValueService;
let tcProvider: ValueService;
let tpcProvider: FakeValueService;
// `inject` uses TestBed's injector
inject([ValueService], (s: ValueService) => testBedProvider = s)();
tcProvider = fixture.debugElement.injector.get(ValueService) as ValueService;
tpcProvider = fixture.debugElement.children[0].injector.get(ValueService) as FakeValueService;
expect(testBedProvider).not.toBe(tcProvider, 'testBed/tc not same providers');
expect(testBedProvider).not.toBe(tpcProvider, 'testBed/tpc not same providers');
expect(testBedProvider instanceof ValueService).toBe(true, 'testBedProvider is ValueService');
expect(tcProvider).toEqual({} as ValueService, 'tcProvider is {}');
expect(tpcProvider instanceof FakeValueService).toBe(true, 'tpcProvider is FakeValueService');
});
it('can access template local variables as references', () => {
const fixture = TestBed.configureTestingModule({
declarations: [ShellComponent, NeedsContentComponent, Child1Component, Child2Component, Child3Component],
})
.overrideComponent(ShellComponent, {
set: {
selector: 'test-shell',
template: `
<needs-content #nc>
<child-1 #content text="My"></child-1>
<child-2 #content text="dog"></child-2>
<child-2 text="has"></child-2>
<child-3 #content text="fleas"></child-3>
<div #content>!</div>
</needs-content>
`
}
})
.createComponent(ShellComponent);
fixture.detectChanges();
// NeedsContentComp is the child of ShellComp
const el = fixture.debugElement.children[0];
const comp = el.componentInstance;
expect(comp.children.toArray().length).toBe(4,
'three different child components and an ElementRef with #content');
expect(el.references['nc']).toBe(comp, '#nc reference to component');
// #docregion custom-predicate
// Filter for DebugElements with a #content reference
const contentRefs = el.queryAll( de => de.references['content']);
// #enddocregion custom-predicate
expect(contentRefs.length).toBe(4, 'elements w/ a #content reference');
});
});
describe('nested (one-deep) component override', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildComponent]
});
});
it('ParentComp should use Fake Child component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child)');
});
});
describe('nested (two-deep) component override', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ParentComponent, FakeChildWithGrandchildComponent, FakeGrandchildComponent]
});
});
it('should use Fake Grandchild component', () => {
const fixture = TestBed.createComponent(ParentComponent);
fixture.detectChanges();
expect(fixture).toHaveText('Parent(Fake Child(Fake Grandchild))');
});
});
describe('lifecycle hooks w/ MyIfParentComp', () => {
let fixture: ComponentFixture<MyIfParentComponent>;
let parent: MyIfParentComponent;
let child: MyIfChildComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [FormsModule],
declarations: [MyIfChildComponent, MyIfParentComponent]
});
fixture = TestBed.createComponent(MyIfParentComponent);
parent = fixture.componentInstance;
});
it('should instantiate parent component', () => {
expect(parent).not.toBeNull('parent component should exist');
});
it('parent component OnInit should NOT be called before first detectChanges()', () => {
expect(parent.ngOnInitCalled).toBe(false);
});
it('parent component OnInit should be called after first detectChanges()', () => {
fixture.detectChanges();
expect(parent.ngOnInitCalled).toBe(true);
});
it('child component should exist after OnInit', () => {
fixture.detectChanges();
getChild();
expect(child instanceof MyIfChildComponent).toBe(true, 'should create child');
});
it('should have called child component\'s OnInit ', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnInitCalled).toBe(true);
});
it('child component called OnChanges once', () => {
fixture.detectChanges();
getChild();
expect(child.ngOnChangesCounter).toBe(1);
});
it('changed parent value flows to child', () => {
fixture.detectChanges();
getChild();
parent.parentValue = 'foo';
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(child.childValue).toBe('foo',
'childValue should eq changed parent value');
});
// must be async test to see child flow to parent
it('changed child value flows to parent', async(() => {
fixture.detectChanges();
getChild();
child.childValue = 'bar';
return new Promise(resolve => {
// Wait one JS engine turn!
setTimeout(() => resolve(), 0);
})
.then(() => {
fixture.detectChanges();
expect(child.ngOnChangesCounter).toBe(2,
'expected 2 changes: initial value and changed value');
expect(parent.parentValue).toBe('bar',
'parentValue should eq changed parent value');
});
}));
it('clicking "Close Child" triggers child OnDestroy', () => {
fixture.detectChanges();
getChild();
const btn = fixture.debugElement.query(By.css('button'));
click(btn);
fixture.detectChanges();
expect(child.ngOnDestroyCalled).toBe(true);
});
////// helpers ///
/**
* Get the MyIfChildComp from parent; fail w/ good message if cannot.
*/
function getChild() {
let childDe: DebugElement; // DebugElement that should hold the MyIfChildComp
// The Hard Way: requires detailed knowledge of the parent template
try {
childDe = fixture.debugElement.children[4].children[0];
} catch (err) { /* we'll report the error */ }
// DebugElement.queryAll: if we wanted all of many instances:
childDe = fixture.debugElement
.queryAll(function (de) { return de.componentInstance instanceof MyIfChildComponent; })[0];
// WE'LL USE THIS APPROACH !
// DebugElement.query: find first instance (if any)
childDe = fixture.debugElement
.query(function (de) { return de.componentInstance instanceof MyIfChildComponent; });
if (childDe && childDe.componentInstance) {
child = childDe.componentInstance;
} else {
fail('Unable to find MyIfChildComp within MyIfParentComp');
}
return child;
}
});
});
////////// Fakes ///////////
@Component({
selector: 'child-1',
template: `Fake Child`
})
class FakeChildComponent { }
@Component({
selector: 'child-1',
template: `Fake Child(<grandchild-1></grandchild-1>)`
})
class FakeChildWithGrandchildComponent { }
@Component({
selector: 'grandchild-1',
template: `Fake Grandchild`
})
class FakeGrandchildComponent { }
@Injectable()
class FakeValueService extends ValueService {
value = 'faked value';
}
@@ -1,15 +0,0 @@
// These unused NgModules keep the Angular Language Service happy.
// The AppModule registers the final versions of these components
import { NgModule } from '@angular/core';
import { AppComponent as app_initial } from './app-initial.component';
@NgModule({ declarations: [ app_initial ] })
export class AppModuleInitial {}
import { BannerComponent as bc_initial } from './banner/banner-initial.component';
@NgModule({ declarations: [ bc_initial ] })
export class BannerModuleInitial {}
import { BannerComponent as bc_external } from './banner/banner-external.component';
@NgModule({ declarations: [ bc_external ] })
export class BannerModuleExternal {}
@@ -1,7 +1,7 @@
import { asyncData, ActivatedRouteStub } from '../../testing';
import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../model/hero';
import { Hero } from '../model';
import { ActivatedRouteStub } from '../../testing';
////////// Tests ////////////////////
@@ -12,21 +12,22 @@ describe('HeroDetailComponent - no TestBed', () => {
let hds: any;
let router: any;
beforeEach((done: DoneFn) => {
expectedHero = {id: 42, name: 'Bubba' };
const activatedRoute = new ActivatedRouteStub({ id: expectedHero.id });
beforeEach((done: any) => {
expectedHero = new Hero(42, 'Bubba');
activatedRoute = new ActivatedRouteStub();
activatedRoute.testParamMap = { id: expectedHero.id };
router = jasmine.createSpyObj('router', ['navigate']);
hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']);
hds.getHero.and.returnValue(asyncData(expectedHero));
hds.saveHero.and.returnValue(asyncData(expectedHero));
hds.getHero.and.returnValue(Promise.resolve(expectedHero));
hds.saveHero.and.returnValue(Promise.resolve(expectedHero));
comp = new HeroDetailComponent(hds, <any> activatedRoute, router);
comp.ngOnInit();
// OnInit calls HDS.getHero; wait for it to get the fake hero
hds.getHero.calls.first().returnValue.subscribe(done);
hds.getHero.calls.first().returnValue.then(done);
});
it('should expose the hero retrieved from the service', () => {
@@ -44,11 +45,11 @@ describe('HeroDetailComponent - no TestBed', () => {
expect(router.navigate.calls.any()).toBe(false, 'router.navigate not called yet');
});
it('should navigate when click save resolves', (done: DoneFn) => {
it('should navigate when click save resolves', (done: any) => {
comp.save();
// waits for async save to complete before navigating
hds.saveHero.calls.first().returnValue
.subscribe(() => {
.then(() => {
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
done();
});
@@ -3,20 +3,21 @@ import {
async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { Router } from '@angular/router';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import {
ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent
ActivatedRoute, ActivatedRouteStub, click, newEvent, Router, RouterStub
} from '../../testing';
import { Hero } from '../model/hero';
import { Hero } from '../model';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroDetailService } from './hero-detail.service';
import { HeroModule } from './hero.module';
////// Testing Vars //////
let activatedRoute: ActivatedRouteStub;
let component: HeroDetailComponent;
let comp: HeroDetailComponent;
let fixture: ComponentFixture<HeroDetailComponent>;
let page: Page;
@@ -31,38 +32,36 @@ describe('HeroDetailComponent', () => {
describe('with SharedModule setup', sharedModuleSetup);
});
///////////////////
////////////////////
function overrideSetup() {
// #docregion hds-spy
class HeroDetailServiceSpy {
testHero: Hero = {id: 42, name: 'Test Hero' };
testHero = new Hero(42, 'Test Hero');
/* emit cloned test hero */
getHero = jasmine.createSpy('getHero').and.callFake(
() => asyncData(Object.assign({}, this.testHero))
() => Promise
.resolve(true)
.then(() => Object.assign({}, this.testHero))
);
/* emit clone of test hero, with changes merged in */
saveHero = jasmine.createSpy('saveHero').and.callFake(
(hero: Hero) => asyncData(Object.assign(this.testHero, hero))
(hero: Hero) => Promise
.resolve(true)
.then(() => Object.assign(this.testHero, hero))
);
}
// #enddocregion hds-spy
// the `id` value is irrelevant because ignored by service stub
beforeEach(() => activatedRoute.setParamMap({ id: 99999 }));
beforeEach(() => activatedRoute.testParamMap = { id: 99999 } );
// #docregion setup-override
beforeEach(async(() => {
const routerSpy = createRouterSpy();
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ HeroModule ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: Router, useValue: routerSpy},
{ provide: Router, useClass: RouterStub},
// #enddocregion setup-override
// HeroDetailService at this level is IRRELEVANT!
{ provide: HeroDetailService, useValue: {} }
@@ -88,7 +87,7 @@ function overrideSetup() {
// #docregion override-tests
let hdsSpy: HeroDetailServiceSpy;
beforeEach(async(() => {
beforeEach( async(() => {
createComponent();
// get the component's injected HeroDetailServiceSpy
hdsSpy = fixture.debugElement.injector.get(HeroDetailService) as any;
@@ -109,7 +108,7 @@ function overrideSetup() {
page.nameInput.value = newName;
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
expect(component.hero.name).toBe(newName, 'component hero has new name');
expect(comp.hero.name).toBe(newName, 'component hero has new name');
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
click(page.saveBtn);
@@ -117,40 +116,36 @@ function overrideSetup() {
tick(); // wait for async save to complete
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
}));
// #enddocregion override-tests
it('fixture injected service is not the component injected service',
// inject gets the service from the fixture
inject([HeroDetailService], (fixtureService: HeroDetailService) => {
inject([HeroDetailService], (service: HeroDetailService) => {
// use `fixture.debugElement.injector` to get service from component
const componentService = fixture.debugElement.injector.get(HeroDetailService);
expect(fixtureService).not.toBe(componentService, 'service injected from fixture');
expect(service).toEqual(<any> {}, 'service injected from fixture');
expect(hdsSpy).toBeTruthy('service injected into component');
}));
}
////////////////////
import { getTestHeroes, TestHeroService, HeroService } from '../model/testing/test-hero.service';
import { HEROES, FakeHeroService } from '../model/testing';
import { HeroService } from '../model';
const firstHero = getTestHeroes()[0];
const firstHero = HEROES[0];
function heroModuleSetup() {
// #docregion setup-hero-module
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ HeroModule ],
// #enddocregion setup-hero-module
// declarations: [ HeroDetailComponent ], // NO! DOUBLE DECLARATION
// #docregion setup-hero-module
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
@@ -161,9 +156,9 @@ function heroModuleSetup() {
describe('when navigate to existing hero', () => {
let expectedHero: Hero;
beforeEach(async(() => {
beforeEach( async(() => {
expectedHero = firstHero;
activatedRoute.setParamMap({ id: expectedHero.id });
activatedRoute.testParamMap = { id: expectedHero.id };
createComponent();
}));
@@ -175,7 +170,7 @@ function heroModuleSetup() {
it('should navigate when click cancel', () => {
click(page.cancelBtn);
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save but not navigate immediately', () => {
@@ -186,31 +181,30 @@ function heroModuleSetup() {
click(page.saveBtn);
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(page.navigateSpy.calls.any()).toBe(false, 'router.navigate not called');
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called');
});
it('should navigate when click save and save resolves', fakeAsync(() => {
click(page.saveBtn);
tick(); // wait for async save to complete
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
}));
// #docregion title-case-pipe
it('should convert hero name to Title Case', () => {
const inputName = 'quick BROWN fox';
const titleCaseName = 'Quick Brown Fox';
const { nameInput, nameDisplay } = page;
// simulate user entering new name into the input box
nameInput.value = inputName;
page.nameInput.value = inputName;
// dispatch a DOM event so that Angular learns of input value change.
nameInput.dispatchEvent(newEvent('input'));
page.nameInput.dispatchEvent(newEvent('input'));
// Tell Angular to update the output span through the title pipe
fixture.detectChanges();
expect(nameDisplay.textContent).toBe(titleCaseName);
expect(page.nameDisplay.textContent).toBe(titleCaseName);
});
// #enddocregion title-case-pipe
// #enddocregion selected-tests
@@ -220,10 +214,10 @@ function heroModuleSetup() {
// #docregion route-no-id
describe('when navigate with no hero id', () => {
beforeEach(async( createComponent ));
beforeEach( async( createComponent ));
it('should have hero.id === 0', () => {
expect(component.hero.id).toBe(0);
expect(comp.hero.id).toBe(0);
});
it('should display empty hero name', () => {
@@ -234,14 +228,14 @@ function heroModuleSetup() {
// #docregion route-bad-id
describe('when navigate to non-existent hero id', () => {
beforeEach(async(() => {
activatedRoute.setParamMap({ id: 99999 });
beforeEach( async(() => {
activatedRoute.testParamMap = { id: 99999 };
createComponent();
}));
it('should try to navigate back to hero list', () => {
expect(page.gotoListSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
});
});
// #enddocregion route-bad-id
@@ -269,25 +263,23 @@ import { TitleCasePipe } from '../shared/title-case.pipe';
function formsModuleSetup() {
// #docregion setup-forms-module
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroDetailComponent, TitleCasePipe ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
}));
// #enddocregion setup-forms-module
it('should display 1st hero\'s name', async(() => {
it('should display 1st hero\'s name', fakeAsync(() => {
const expectedHero = firstHero;
activatedRoute.setParamMap({ id: expectedHero.id });
activatedRoute.testParamMap = { id: expectedHero.id };
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
@@ -299,25 +291,23 @@ import { SharedModule } from '../shared/shared.module';
function sharedModuleSetup() {
// #docregion setup-shared-module
beforeEach(async(() => {
const routerSpy = createRouterSpy();
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [ HeroDetailComponent ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
]
})
.compileComponents();
}));
// #enddocregion setup-shared-module
it('should display 1st hero\'s name', async(() => {
it('should display 1st hero\'s name', fakeAsync(() => {
const expectedHero = firstHero;
activatedRoute.setParamMap({ id: expectedHero.id });
activatedRoute.testParamMap = { id: expectedHero.id };
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
@@ -330,51 +320,45 @@ function sharedModuleSetup() {
/** Create the HeroDetailComponent, initialize it, set test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroDetailComponent);
component = fixture.componentInstance;
page = new Page(fixture);
comp = fixture.componentInstance;
page = new Page();
// 1st change detection triggers ngOnInit which gets a hero
fixture.detectChanges();
return fixture.whenStable().then(() => {
// 2nd change detection displays the async-fetched hero
fixture.detectChanges();
page.addPageElements();
});
}
// #enddocregion create-component
// #docregion page
class Page {
// getter properties wait to query the DOM until called.
get buttons() { return this.queryAll<HTMLButtonElement>('button'); }
get saveBtn() { return this.buttons[0]; }
get cancelBtn() { return this.buttons[1]; }
get nameDisplay() { return this.query<HTMLElement>('span'); }
get nameInput() { return this.query<HTMLInputElement>('input'); }
gotoSpy: jasmine.Spy;
navSpy: jasmine.Spy;
gotoListSpy: jasmine.Spy;
navigateSpy: jasmine.Spy;
saveBtn: DebugElement;
cancelBtn: DebugElement;
nameDisplay: HTMLElement;
nameInput: HTMLInputElement;
constructor(fixture: ComponentFixture<HeroDetailComponent>) {
// get the navigate spy from the injected router spy object
const routerSpy = <any> fixture.debugElement.injector.get(Router);
this.navigateSpy = routerSpy.navigate;
// spy on component's `gotoList()` method
const component = fixture.componentInstance;
this.gotoListSpy = spyOn(component, 'gotoList').and.callThrough();
constructor() {
const router = TestBed.get(Router); // get router from root injector
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
this.navSpy = spyOn(router, 'navigate');
}
//// query helpers ////
private query<T>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
private queryAll<T>(selector: string): T[] {
return fixture.nativeElement.querySelectorAll(selector);
/** Add page elements after hero arrives */
addPageElements() {
if (comp.hero) {
// have a hero so these elements are now in the DOM
const buttons = fixture.debugElement.queryAll(By.css('button'));
this.saveBtn = buttons[0];
this.cancelBtn = buttons[1];
this.nameDisplay = fixture.debugElement.query(By.css('span')).nativeElement;
this.nameInput = fixture.debugElement.query(By.css('input')).nativeElement;
}
}
}
// #enddocregion page
function createRouterSpy() {
return jasmine.createSpyObj('Router', ['navigate']);
}
@@ -2,6 +2,7 @@
// #docplaster
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import 'rxjs/add/operator/map';
import { Hero } from '../model/hero';
import { HeroDetailService } from './hero-detail.service';
@@ -28,18 +29,18 @@ export class HeroDetailComponent implements OnInit {
// #docregion ng-on-init
ngOnInit(): void {
// get hero when `id` param changes
this.route.paramMap.subscribe(pmap => this.getHero(pmap.get('id')));
this.route.paramMap.subscribe(p => this.getHero(p.has('id') && p.get('id')));
}
// #enddocregion ng-on-init
private getHero(id: string): void {
// when no id or id===0, create new blank hero
// when no id or id===0, create new hero
if (!id) {
this.hero = { id: 0, name: '' } as Hero;
this.hero = new Hero();
return;
}
this.heroDetailService.getHero(id).subscribe(hero => {
this.heroDetailService.getHero(id).then(hero => {
if (hero) {
this.hero = hero;
} else {
@@ -49,7 +50,7 @@ export class HeroDetailComponent implements OnInit {
}
save(): void {
this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());
this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
}
cancel() { this.gotoList(); }
@@ -1,8 +1,5 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { map } from 'rxjs/operators';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@@ -13,15 +10,13 @@ export class HeroDetailService {
// #enddocregion prototype
// Returns a clone which caller may modify safely
getHero(id: number | string): Observable<Hero> {
getHero(id: number | string): Promise<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
return this.heroService.getHero(id).pipe(
map(hero => {
return hero ? Object.assign({}, hero) : null; // clone or null
})
);
return this.heroService.getHero(id).then(hero => {
return hero ? Object.assign({}, hero) : null; // clone or null
});
}
saveHero(hero: Hero) {
@@ -6,7 +6,7 @@
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 15em;
width: 10em;
}
.heroes li {
cursor: pointer;
@@ -4,18 +4,15 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import { addMatchers, newEvent, Router, RouterStub
} from '../../testing';
import { addMatchers, newEvent } from '../../testing';
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
import { HEROES, FakeHeroService } from '../model/testing';
import { HeroModule } from './hero.module';
import { HeroListComponent } from './hero-list.component';
import { HighlightDirective } from '../shared/highlight.directive';
import { HeroService } from '../model/hero.service';
const HEROES = getTestHeroes();
import { HeroService } from '../model';
let comp: HeroListComponent;
let fixture: ComponentFixture<HeroListComponent>;
@@ -25,15 +22,13 @@ let page: Page;
describe('HeroListComponent', () => {
beforeEach(async(() => {
beforeEach( async(() => {
addMatchers();
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
imports: [HeroModule],
providers: [
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy}
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub}
]
})
.compileComponents()
@@ -130,14 +125,15 @@ class Page {
navSpy: jasmine.Spy;
constructor() {
const heroRowNodes = fixture.nativeElement.querySelectorAll('li');
this.heroRows = Array.from(heroRowNodes);
this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement);
// Find the first element with an attached HighlightDirective
this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective));
// Get the component's injected router navigation spy
const routerSpy = fixture.debugElement.injector.get(Router);
this.navSpy = routerSpy.navigate as jasmine.Spy;
// Get the component's injected router and spy on it
const router = fixture.debugElement.injector.get(Router);
this.navSpy = spyOn(router, 'navigate');
};
}
@@ -1,8 +1,6 @@
import { Component, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Hero } from '../model/hero';
import { HeroService } from '../model/hero.service';
@@ -12,7 +10,7 @@ import { HeroService } from '../model/hero.service';
styleUrls: [ './hero-list.component.css' ]
})
export class HeroListComponent implements OnInit {
heroes: Observable<Hero[]>;
heroes: Promise<Hero[]>;
selectedHero: Hero;
constructor(
@@ -1,26 +0,0 @@
// #docregion , init
import { InMemoryDbService } from 'angular-in-memory-web-api';
import { QUOTES } from './twain/twain.data';
// Adjust to reduce number of quotes
const maxQuotes = Infinity; // 0;
/** Create in-memory database of heroes and quotes */
export class InMemoryDataService implements InMemoryDbService {
createDb() {
const heroes = [
{ id: 11, name: 'Mr. Nice' },
{ id: 12, name: 'Narco' },
{ id: 13, name: 'Bombasto' },
{ id: 14, name: 'Celeritas' },
{ id: 15, name: 'Magneta' },
{ id: 16, name: 'RubberMan' },
{ id: 17, name: 'Dynama' },
{ id: 18, name: 'Dr IQ' },
{ id: 19, name: 'Magma' },
{ id: 20, name: 'Tornado' }
];
return { heroes, quotes: QUOTES.slice(0, maxQuotes) };
}
}
@@ -1,215 +0,0 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpResponse, HttpErrorResponse } from '@angular/common/http';
import { asyncData, asyncError } from '../../testing/async-observable-helpers';
import { Hero } from './hero';
import { HeroService } from './hero.service';
describe ('HeroesService (with spies)', () => {
// #docregion test-with-spies
let httpClientSpy: { get: jasmine.Spy };
let heroService: HeroService;
beforeEach(() => {
// Todo: spy on other methods too
httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']);
heroService = new HeroService(<any> httpClientSpy);
});
it('should return expected heroes (HttpClient called once)', () => {
const expectedHeroes: Hero[] =
[{ id: 1, name: 'A' }, { id: 2, name: 'B' }];
httpClientSpy.get.and.returnValue(asyncData(expectedHeroes));
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'expected heroes'),
fail
);
expect(httpClientSpy.get.calls.count()).toBe(1, 'one call');
});
it('should return an error when the server returns a 404', () => {
const errorResponse = new HttpErrorResponse({
error: 'test 404 error',
status: 404, statusText: 'Not Found'
});
httpClientSpy.get.and.returnValue(asyncError(errorResponse));
heroService.getHeroes().subscribe(
heroes => fail('expected an error, not heroes'),
error => expect(error.message).toContain('test 404 error')
);
});
// #enddocregion test-with-spies
});
describe('HeroesService (with mocks)', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
let heroService: HeroService;
beforeEach(() => {
TestBed.configureTestingModule({
// Import the HttpClient mocking services
imports: [ HttpClientTestingModule ],
// Provide the service-under-test
providers: [ HeroService ]
});
// Inject the http, test controller, and service-under-test
// as they will be referenced by each test.
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
heroService = TestBed.get(HeroService);
});
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
/// HeroService method tests begin ///
describe('#getHeroes', () => {
let expectedHeroes: Hero[];
beforeEach(() => {
heroService = TestBed.get(HeroService);
expectedHeroes = [
{ id: 1, name: 'A' },
{ id: 2, name: 'B' },
] as Hero[];
});
it('should return expected heroes (called once)', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
// HeroService should have made one request to GET heroes from expected URL
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('GET');
// Respond with the mock heroes
req.flush(expectedHeroes);
});
it('should be OK returning no heroes', () => {
heroService.getHeroes().subscribe(
heroes => expect(heroes.length).toEqual(0, 'should have empty heroes array'),
fail
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
req.flush([]); // Respond with no heroes
});
it('should turn 404 into a user-friendly error', () => {
const msg = 'Deliberate 404';
heroService.getHeroes().subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(msg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
it('should return expected heroes (called multiple times)', () => {
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe();
heroService.getHeroes().subscribe(
heroes => expect(heroes).toEqual(expectedHeroes, 'should return expected heroes'),
fail
);
const requests = httpTestingController.match(heroService.heroesUrl);
expect(requests.length).toEqual(3, 'calls to getHeroes()');
// Respond to each request with different mock hero results
requests[0].flush([]);
requests[1].flush([{id: 1, name: 'bob'}]);
requests[2].flush(expectedHeroes);
});
});
describe('#updateHero', () => {
// Expecting the query form of URL so should not 404 when id not found
const makeUrl = (id: number) => `${heroService.heroesUrl}/?id=${id}`;
it('should update a hero and return it', () => {
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
data => expect(data).toEqual(updateHero, 'should return the hero'),
fail
);
// HeroService should have made one request to PUT hero
const req = httpTestingController.expectOne(heroService.heroesUrl);
expect(req.request.method).toEqual('PUT');
expect(req.request.body).toEqual(updateHero);
// Expect server to return the hero after PUT
const expectedResponse = new HttpResponse(
{ status: 200, statusText: 'OK', body: updateHero });
req.event(expectedResponse);
});
it('should turn 404 error into user-facing error', () => {
const msg = 'Deliberate 404';
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(msg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// respond with a 404 and the error message in the body
req.flush(msg, {status: 404, statusText: 'Not Found'});
});
// #docregion network-error
it('should turn network error into user-facing error', () => {
const emsg = 'simulated network error';
const updateHero: Hero = { id: 1, name: 'A' };
heroService.updateHero(updateHero).subscribe(
heroes => fail('expected to fail'),
error => expect(error.message).toContain(emsg)
);
const req = httpTestingController.expectOne(heroService.heroesUrl);
// Create mock ErrorEvent, raised when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc
const errorEvent = new ErrorEvent('so sad', {
message: emsg,
// #enddocregion network-error
// The rest of this is optional and not used.
// Just showing that you could provide this too.
filename: 'HeroService.ts',
lineno: 42,
colno: 21
// #docregion network-error
});
// Respond with mock error
req.error(errorEvent);
});
// #enddocregion network-error
});
// TODO: test other HeroService methods
});
@@ -1,98 +1,30 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, map, tap } from 'rxjs/operators';
import { Hero } from './hero';
const httpOptions = {
headers: new HttpHeaders({ 'Content-Type': 'application/json' })
};
import { Hero } from './hero';
import { HEROES } from './test-heroes';
@Injectable()
/** Dummy HeroService. Pretend it makes real http requests */
export class HeroService {
readonly heroesUrl = 'api/heroes'; // URL to web api
constructor(private http: HttpClient) { }
/** GET heroes from the server */
getHeroes (): Observable<Hero[]> {
return this.http.get<Hero[]>(this.heroesUrl)
.pipe(
tap(heroes => this.log(`fetched heroes`)),
catchError(this.handleError('getHeroes'))
) as Observable<Hero[]>;
getHeroes() {
return Promise.resolve(HEROES);
}
/** GET hero by id. Return `undefined` when id not found */
getHero<Data>(id: number | string): Observable<Hero> {
getHero(id: number | string): Promise<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
const url = `${this.heroesUrl}/?id=${id}`;
return this.http.get<Hero[]>(url)
.pipe(
map(heroes => heroes[0]), // returns a {0|1} element array
tap(h => {
const outcome = h ? `fetched` : `did not find`;
this.log(`${outcome} hero id=${id}`);
}),
catchError(this.handleError<Hero>(`getHero id=${id}`))
);
}
//////// Save methods //////////
/** POST: add a new hero to the server */
addHero (hero: Hero): Observable<Hero> {
return this.http.post<Hero>(this.heroesUrl, hero, httpOptions).pipe(
tap((hero: Hero) => this.log(`added hero w/ id=${hero.id}`)),
catchError(this.handleError<Hero>('addHero'))
);
}
/** DELETE: delete the hero from the server */
deleteHero (hero: Hero | number): Observable<Hero> {
const id = typeof hero === 'number' ? hero : hero.id;
const url = `${this.heroesUrl}/${id}`;
return this.http.delete<Hero>(url, httpOptions).pipe(
tap(_ => this.log(`deleted hero id=${id}`)),
catchError(this.handleError<Hero>('deleteHero'))
return this.getHeroes().then(
heroes => heroes.find(hero => hero.id === id)
);
}
/** PUT: update the hero on the server */
updateHero (hero: Hero): Observable<any> {
return this.http.put(this.heroesUrl, hero, httpOptions).pipe(
tap(_ => this.log(`updated hero id=${hero.id}`)),
catchError(this.handleError<any>('updateHero'))
);
}
/**
* Returns a function that handles Http operation failures.
* This error handler lets the app continue to run as if no error occurred.
* @param operation - name of the operation that failed
*/
private handleError<T> (operation = 'operation') {
return (error: HttpErrorResponse): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
const message = (error.error instanceof ErrorEvent) ?
error.error.message :
`server returned code ${error.status} with body "${error.error}"`;
// TODO: better job of transforming error for user consumption
throw new Error(`${operation} failed: ${message}`);
};
}
private log(message: string) {
console.log('HeroService: ' + message);
updateHero(hero: Hero): Promise<Hero> {
return this.getHero(hero.id).then(h => {
if (!h) {
throw new Error(`Hero ${hero.id} not found`);
}
return Object.assign(h, hero);
});
}
}
@@ -0,0 +1,20 @@
// #docregion
import { Hero } from './hero';
describe('Hero', () => {
it('has name', () => {
const hero = new Hero(1, 'Super Cat');
expect(hero.name).toBe('Super Cat');
});
it('has id', () => {
const hero = new Hero(1, 'Super Cat');
expect(hero.id).toBe(1);
});
it('can clone itself', () => {
const hero = new Hero(1, 'Super Cat');
const clone = hero.clone();
expect(hero).toEqual(clone);
});
});
@@ -1,8 +1,4 @@
export interface Hero {
id: number;
name: string;
export class Hero {
constructor(public id = 0, public name = '') { }
clone() { return new Hero(this.id, this.name); }
}
// SystemJS bug:
// TS file must export something real in JS, not just interfaces
export const _dummy = undefined;
@@ -1,6 +1,3 @@
/**
* Test the HeroService when implemented with the OLD HttpModule
*/
import {
async, inject, TestBed
} from '@angular/core/testing';
@@ -15,11 +12,14 @@ import {
} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, tap } from 'rxjs/operators';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/toPromise';
import { Hero } from './hero';
import { HttpHeroService } from './http-hero.service';
import { HttpHeroService as HeroService } from './http-hero.service';
const makeHeroData = () => [
{ id: 1, name: 'Windstorm' },
@@ -29,100 +29,99 @@ const makeHeroData = () => [
] as Hero[];
//////// Tests /////////////
describe('HttpHeroService (using old HttpModule)', () => {
let backend: MockBackend;
let service: HttpHeroService;
describe('Http-HeroService (mockBackend)', () => {
beforeEach( () => {
beforeEach( async(() => {
TestBed.configureTestingModule({
imports: [ HttpModule ],
providers: [
HttpHeroService,
HeroService,
{ provide: XHRBackend, useClass: MockBackend }
]
});
});
})
.compileComponents();
}));
it('can instantiate service via DI', () => {
service = TestBed.get(HttpHeroService);
expect(service instanceof HttpHeroService).toBe(true);
});
it('can instantiate service when inject service',
inject([HeroService], (service: HeroService) => {
expect(service instanceof HeroService).toBe(true);
}));
it('can instantiate service with "new"', () => {
const http = TestBed.get(Http);
it('can instantiate service with "new"', inject([Http], (http: Http) => {
expect(http).not.toBeNull('http should be provided');
let service = new HttpHeroService(http);
expect(service instanceof HttpHeroService).toBe(true, 'new service should be ok');
});
let service = new HeroService(http);
expect(service instanceof HeroService).toBe(true, 'new service should be ok');
}));
it('can provide the mockBackend as XHRBackend', () => {
const backend = TestBed.get(XHRBackend);
expect(backend).not.toBeNull('backend should be provided');
});
it('can provide the mockBackend as XHRBackend',
inject([XHRBackend], (backend: MockBackend) => {
expect(backend).not.toBeNull('backend should be provided');
}));
describe('when getHeroes', () => {
let fakeHeroes: Hero[];
let http: Http;
let response: Response;
let backend: MockBackend;
let service: HeroService;
let fakeHeroes: Hero[];
let response: Response;
beforeEach(() => {
beforeEach(inject([Http, XHRBackend], (http: Http, be: MockBackend) => {
backend = be;
service = new HeroService(http);
fakeHeroes = makeHeroData();
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
response = new Response(options);
}));
backend = TestBed.get(XHRBackend);
http = TestBed.get(Http);
it('should have expected fake heroes (then)', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
service = new HttpHeroService(http);
fakeHeroes = makeHeroData();
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
response = new Response(options);
});
service.getHeroes().toPromise()
// .then(() => Promise.reject('deliberate'))
.then(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
});
})));
it('should have expected fake heroes (then)', () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
it('should have expected fake heroes (Observable.do)', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
service.getHeroes().toPromise()
// .then(() => Promise.reject('deliberate'))
.then(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
})
.catch(fail);
});
it('should have expected fake heroes (Observable tap)', () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
service.getHeroes().subscribe(
heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
},
fail
);
});
service.getHeroes()
.do(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
})
.toPromise();
})));
it('should be OK returning no heroes', () => {
let resp = new Response(new ResponseOptions({status: 200, body: {data: []}}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
it('should be OK returning no heroes', async(inject([], () => {
let resp = new Response(new ResponseOptions({status: 200, body: {data: []}}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
service.getHeroes().subscribe(
heroes => {
expect(heroes.length).toBe(0, 'should have no heroes');
},
fail
);
});
service.getHeroes()
.do(heroes => {
expect(heroes.length).toBe(0, 'should have no heroes');
})
.toPromise();
})));
it('should treat 404 as an Observable error', () => {
let resp = new Response(new ResponseOptions({status: 404}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
it('should treat 404 as an Observable error', async(inject([], () => {
let resp = new Response(new ResponseOptions({status: 404}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
service.getHeroes().subscribe(
heroes => fail('should not respond with heroes'),
err => {
expect(err).toMatch(/Bad response status/, 'should catch bad response status code');
return of(null); // failure is the expected test result
});
});
service.getHeroes()
.do(heroes => {
fail('should not respond with heroes');
})
.catch(err => {
expect(err).toMatch(/Bad response status/, 'should catch bad response status code');
return Observable.of(null); // failure is the expected test result
})
.toPromise();
})));
});
});
@@ -1,4 +1,3 @@
// The OLD Http module. See HeroService for use of the current HttpClient
// #docplaster
// #docregion
import { Injectable } from '@angular/core';
@@ -6,9 +5,12 @@ import { Http, Response } from '@angular/http';
import { Headers, RequestOptions } from '@angular/http';
import { Hero } from './hero';
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { catchError, map, tap } from 'rxjs/operators';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
@Injectable()
export class HttpHeroService {
@@ -17,17 +19,16 @@ export class HttpHeroService {
constructor (private http: Http) {}
getHeroes (): Observable<Hero[]> {
return this.http.get(this._heroesUrl).pipe(
map(this.extractData),
// tap(data => console.log(data)), // eyeball results in the console
catchError(this.handleError)
);
return this.http.get(this._heroesUrl)
.map(this.extractData)
// .do(data => console.log(data)) // eyeball results in the console
.catch(this.handleError);
}
getHero(id: number | string) {
return this.http.get('app/heroes/?id=${id}').pipe(
map((r: Response) => r.json().data as Hero[])
);
return this.http
.get('app/heroes/?id=${id}')
.map((r: Response) => r.json().data as Hero[]);
}
addHero (name: string): Observable<Hero> {
@@ -35,10 +36,9 @@ export class HttpHeroService {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.post(this._heroesUrl, body, options).pipe(
map(this.extractData),
catchError(this.handleError)
);
return this.http.post(this._heroesUrl, body, options)
.map(this.extractData)
.catch(this.handleError);
}
updateHero (hero: Hero): Observable<Hero> {
@@ -46,10 +46,9 @@ export class HttpHeroService {
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
return this.http.put(this._heroesUrl, body, options).pipe(
map(this.extractData),
catchError(this.handleError)
);
return this.http.put(this._heroesUrl, body, options)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
@@ -64,6 +63,6 @@ export class HttpHeroService {
// In a real world app, we might send the error to remote logging infrastructure
let errMsg = error.message || 'Server error';
console.error(errMsg); // log to console instead
return new ErrorObservable(errMsg);
return Observable.throw(errMsg);
}
}
@@ -0,0 +1,11 @@
// #docregion
import { Hero } from './hero';
export const HEROES: Hero[] = [
new Hero(11, 'Mr. Nice'),
new Hero(12, 'Narco'),
new Hero(13, 'Bombasto'),
new Hero(14, 'Celeritas'),
new Hero(15, 'Magneta'),
new Hero(16, 'RubberMan')
];
@@ -0,0 +1,41 @@
// re-export for tester convenience
export { Hero } from '../hero';
export { HeroService } from '../hero.service';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
export const HEROES: Hero[] = [
new Hero(41, 'Bob'),
new Hero(42, 'Carol'),
new Hero(43, 'Ted'),
new Hero(44, 'Alice'),
new Hero(45, 'Speedy'),
new Hero(46, 'Stealthy')
];
export class FakeHeroService implements HeroService {
heroes = HEROES.map(h => h.clone());
lastPromise: Promise<any>; // remember so we can spy on promise calls
getHero(id: number | string) {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
let hero = this.heroes.find(h => h.id === id);
return this.lastPromise = Promise.resolve(hero);
}
getHeroes() {
return this.lastPromise = Promise.resolve<Hero[]>(this.heroes);
}
updateHero(hero: Hero): Promise<Hero> {
return this.lastPromise = this.getHero(hero.id).then(h => {
return h ?
Object.assign(h, hero) :
Promise.reject(`Hero ${hero.id} not found`) as any as Promise<Hero>;
});
}
}
@@ -1,192 +0,0 @@
// #docplaster
// #docregion imports
// Http testing module and mocking controller
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
// Other imports
import { TestBed } from '@angular/core/testing';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
// #enddocregion imports
import { HttpHeaders } from '@angular/common/http';
interface Data {
name: string;
}
const testUrl = '/data';
// #docregion setup
describe('HttpClient testing', () => {
let httpClient: HttpClient;
let httpTestingController: HttpTestingController;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ HttpClientTestingModule ]
});
// Inject the http service and test controller for each test
httpClient = TestBed.get(HttpClient);
httpTestingController = TestBed.get(HttpTestingController);
});
// #enddocregion setup
// #docregion afterEach
afterEach(() => {
// After every test, assert that there are no more pending requests.
httpTestingController.verify();
});
// #enddocregion afterEach
// #docregion setup
/// Tests begin ///
// #enddocregion setup
// #docregion get-test
it('can test HttpClient.get', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request
httpClient.get<Data>(testUrl)
.subscribe(data =>
// When observable resolves, result should match test data
expect(data).toEqual(testData)
);
// The following `expectOne()` will match the request's URL.
// If no requests or multiple requests matched that URL
// `expectOne()` would throw.
const req = httpTestingController.expectOne('/data');
// Assert that the request is a GET.
expect(req.request.method).toEqual('GET');
// Respond with mock data, causing Observable to resolve.
// Subscribe callback asserts that correct data was returned.
req.flush(testData);
// Finally, assert that there are no outstanding requests.
httpTestingController.verify();
});
// #enddocregion get-test
it('can test HttpClient.get with matching header', () => {
const testData: Data = {name: 'Test Data'};
// Make an HTTP GET request with specific header
httpClient.get<Data>(testUrl, {
headers: new HttpHeaders({'Authorization': 'my-auth-token'})
})
.subscribe(data =>
expect(data).toEqual(testData)
);
// Find request with a predicate function.
// #docregion predicate
// Expect one request with an authorization header
const req = httpTestingController.expectOne(
req => req.headers.has('Authorization')
);
// #enddocregion predicate
req.flush(testData);
});
it('can test multiple requests', () => {
let testData: Data[] = [
{ name: 'bob' }, { name: 'carol' },
{ name: 'ted' }, { name: 'alice' }
];
// Make three requests in a row
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d.length).toEqual(0, 'should have no data'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual([testData[0]], 'should be one element array'));
httpClient.get<Data[]>(testUrl)
.subscribe(d => expect(d).toEqual(testData, 'should be expected data'));
// #docregion multi-request
// get all pending requests that match the given URL
const requests = httpTestingController.match(testUrl);
expect(requests.length).toEqual(3);
// Respond to each request with different results
requests[0].flush([]);
requests[1].flush([testData[0]]);
requests[2].flush(testData);
// #enddocregion multi-request
});
// #docregion 404
it('can test for 404 error', () => {
const emsg = 'deliberate 404 error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the 404 error'),
(error: HttpErrorResponse) => {
expect(error.status).toEqual(404, 'status');
expect(error.error).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Respond with mock error
req.flush(emsg, { status: 404, statusText: 'Not Found' });
});
// #enddocregion 404
// #docregion network-error
it('can test for network error', () => {
const emsg = 'simulated network error';
httpClient.get<Data[]>(testUrl).subscribe(
data => fail('should have failed with the network error'),
(error: HttpErrorResponse) => {
expect(error.error.message).toEqual(emsg, 'message');
}
);
const req = httpTestingController.expectOne(testUrl);
// Create mock ErrorEvent, raised when something goes wrong at the network level.
// Connection timeout, DNS error, offline, etc
const errorEvent = new ErrorEvent('so sad', {
message: emsg,
// #enddocregion network-error
// The rest of this is optional and not used.
// Just showing that you could provide this too.
filename: 'HeroService.ts',
lineno: 42,
colno: 21
// #docregion network-error
});
// Respond with mock error
req.error(errorEvent);
});
// #enddocregion network-error
it('httpTestingController.verify should fail if HTTP response not simulated', () => {
// Sends request
httpClient.get('some/api').subscribe();
// verify() should fail because haven't handled the pending request.
expect(() => httpTestingController.verify()).toThrow();
// Now get and flush the request so that afterEach() doesn't fail
const req = httpTestingController.expectOne('some/api');
req.flush(null);
});
// Proves that verify in afterEach() really would catch error
// if test doesn't simulate the HTTP response.
//
// Must disable this test because can't catch an error in an afterEach().
// Uncomment if you want to confirm that afterEach() does the job.
// it('afterEach() should fail when HTTP response not simulated',() => {
// // Sends request which is never handled by this test
// httpClient.get('some/api').subscribe();
// });
// #docregion setup
});
// #enddocregion setup
@@ -1,61 +0,0 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { asyncData } from '../../../testing';
import { map } from 'rxjs/operators';
// re-export for tester convenience
export { Hero } from '../hero';
export { HeroService } from '../hero.service';
export { getTestHeroes } from './test-heroes';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
import { getTestHeroes } from './test-heroes';
@Injectable()
/**
* FakeHeroService pretends to make real http requests.
* implements only as much of HeroService as is actually consumed by the app
*/
export class TestHeroService extends HeroService {
constructor() {
super(null);
}
heroes = getTestHeroes();
lastResult: Observable<any>; // result from last method call
addHero(hero: Hero): Observable<Hero> {
throw new Error('Method not implemented.');
}
deleteHero(hero: number | Hero): Observable<Hero> {
throw new Error('Method not implemented.');
}
getHeroes(): Observable<Hero[]> {
return this.lastResult = asyncData(this.heroes);
}
getHero(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
let hero = this.heroes.find(h => h.id === id);
return this.lastResult = asyncData(hero);
}
updateHero(hero: Hero): Observable<Hero> {
return this.lastResult = this.getHero(hero.id).pipe(
map(h => {
if (h) {
return Object.assign(h, hero);
}
throw new Error(`Hero ${hero.id} not found`);
})
);
}
}
@@ -1,13 +0,0 @@
import { Hero } from '../hero';
/** return fresh array of test heroes */
export function getTestHeroes(): Hero[] {
return [
{id: 41, name: 'Bob' },
{id: 42, name: 'Carol' },
{id: 43, name: 'Ted' },
{id: 44, name: 'Alice' },
{id: 45, name: 'Speedy' },
{id: 46, name: 'Stealthy' }
];
}
@@ -4,16 +4,12 @@ import { FormsModule } from '@angular/forms';
import { HighlightDirective } from './highlight.directive';
import { TitleCasePipe } from './title-case.pipe';
import { TwainComponent } from './twain.component';
@NgModule({
imports: [ CommonModule ],
exports: [
CommonModule,
// SharedModule importers won't have to import FormsModule too
FormsModule,
HighlightDirective,
TitleCasePipe
],
declarations: [ HighlightDirective, TitleCasePipe ]
imports: [ CommonModule ],
exports: [ CommonModule, FormsModule,
HighlightDirective, TitleCasePipe, TwainComponent ],
declarations: [ HighlightDirective, TitleCasePipe, TwainComponent ]
})
export class SharedModule { }
@@ -0,0 +1,92 @@
// #docplaster
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
describe('TwainComponent', () => {
let comp: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let spy: jasmine.Spy;
let de: DebugElement;
let el: HTMLElement;
let twainService: TwainService; // the actually injected service
const testQuote = 'Test Quote';
// #docregion setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [ TwainService ],
});
fixture = TestBed.createComponent(TwainComponent);
comp = fixture.componentInstance;
// TwainService actually injected into the component
twainService = fixture.debugElement.injector.get(TwainService);
// Setup spy on the `getQuote` method
// #docregion spy
spy = spyOn(twainService, 'getQuote')
.and.returnValue(Promise.resolve(testQuote));
// #enddocregion spy
// Get the Twain quote element by CSS selector (e.g., by class name)
de = fixture.debugElement.query(By.css('.twain'));
el = de.nativeElement;
});
// #enddocregion setup
// #docregion tests
it('should not show quote before OnInit', () => {
expect(el.textContent).toBe('', 'nothing displayed');
expect(spy.calls.any()).toBe(false, 'getQuote not yet called');
});
it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
expect(el.textContent).toBe('...', 'no quote yet');
expect(spy.calls.any()).toBe(true, 'getQuote called');
});
// #docregion async-test
it('should show quote after getQuote promise (async)', async(() => {
fixture.detectChanges();
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
});
}));
// #enddocregion async-test
// #docregion fake-async-test
it('should show quote after getQuote promise (fakeAsync)', fakeAsync(() => {
fixture.detectChanges();
tick(); // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
}));
// #enddocregion fake-async-test
// #enddocregion tests
// #docregion done-test
it('should show quote after getQuote promise (done)', (done: any) => {
fixture.detectChanges();
// get the spy promise and wait for it to resolve
spy.calls.mostRecent().returnValue.then(() => {
fixture.detectChanges(); // update view with quote
expect(el.textContent).toBe(testQuote);
done();
});
});
// #enddocregion done-test
});
@@ -0,0 +1,116 @@
// #docplaster
// When AppComponent learns to present quote with intervalTimer
import { async, discardPeriodicTasks, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TwainService } from './model';
import { TwainComponent } from './twain.component';
xdescribe('TwainComponent', () => {
let comp: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
const quotes = [
'Test Quote 1',
'Test Quote 2',
'Test Quote 3'
];
let spy: jasmine.Spy;
let twainEl: DebugElement; // the element with the Twain quote
let twainService: TwainService; // the actually injected service
function getQuote() { return twainEl.nativeElement.textContent; }
// #docregion setup
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [ TwainService ],
});
fixture = TestBed.createComponent(TwainComponent);
comp = fixture.componentInstance;
// TwainService actually injected into the component
twainService = fixture.debugElement.injector.get(TwainService);
// Setup spy on the `getQuote` method
spy = spyOn(twainService, 'getQuote')
.and.returnValues(...quotes.map(q => Promise.resolve(q)));
// Get the Twain quote element by CSS selector (e.g., by class name)
twainEl = fixture.debugElement.query(By.css('.twain'));
});
afterEach(() => {
// destroy component to stop the component timer
fixture.destroy();
});
// #enddocregion setup
// #docregion tests
it('should not show quote before OnInit', () => {
expect(getQuote()).toBe('');
});
it('should still not show quote after component initialized', () => {
// because the getQuote service is async
fixture.detectChanges(); // trigger data binding
expect(getQuote()).toContain('not initialized');
});
// WIP
// If go this way, add jasmine.clock().uninstall(); to afterEach
// it('should show quote after Angular "settles"', async(() => {
// //jasmine.clock().install();
// fixture.detectChanges(); // trigger data binding
// fixture.whenStable().then(() => {
// fixture.detectChanges(); // update view with the quote
// expect(getQuote()).toBe(quotes[0]);
// });
// // jasmine.clock().tick(5000);
// // fixture.whenStable().then(() => {
// // fixture.detectChanges(); // update view with the quote
// // expect(getQuote()).toBe(quotes[1]);
// // });
// }));
it('should show quote after getQuote promise returns', fakeAsync(() => {
fixture.detectChanges(); // trigger data binding
tick(); // wait for first async getQuote to return
fixture.detectChanges(); // update view with the quote
expect(getQuote()).toBe(quotes[0]);
// destroy component to stop the component timer before test ends
// else test errors because still have timer in the queue
fixture.destroy();
}));
it('should show 2nd quote after 5 seconds pass', fakeAsync(() => {
fixture.detectChanges(); // trigger data binding
tick(5000); // wait for second async getQuote to return
fixture.detectChanges(); // update view with the quote
expect(getQuote()).toBe(quotes[1]);
// still have intervalTimer queuing requres
// discardPeriodicTasks() else test errors
discardPeriodicTasks();
}));
fit('should show 3rd quote after 10 seconds pass', fakeAsync(() => {
fixture.detectChanges(); // trigger data binding
tick(5000); // wait for second async getQuote to return
fixture.detectChanges(); // update view with the 2nd quote
tick(5000); // wait for third async getQuote to return
fixture.detectChanges(); // update view with the 3rd quote
expect(getQuote()).toBe(quotes[2]);
// still have intervalTimer queuing requres
// discardPeriodicTasks() else test errors
discardPeriodicTasks();
}));
// #enddocregion tests
});
@@ -0,0 +1,27 @@
// #docregion
import { Component, OnInit, OnDestroy } from '@angular/core';
import { TwainService } from './twain.service';
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit, OnDestroy {
intervalId: number;
quote = '-- not initialized yet --';
constructor(private twainService: TwainService) { }
getQuote() {
this.twainService.getQuote().then(quote => this.quote = quote);
}
ngOnInit(): void {
this.getQuote();
this.intervalId = window.setInterval(() => this.getQuote(), 5000);
}
ngOnDestroy(): void {
clearInterval(this.intervalId);
}
}
@@ -0,0 +1,20 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { TwainService } from './twain.service';
// #docregion component
@Component({
selector: 'twain-quote',
template: '<p class="twain"><i>{{quote}}</i></p>'
})
export class TwainComponent implements OnInit {
intervalId: number;
quote = '...';
constructor(private twainService: TwainService) { }
ngOnInit(): void {
this.twainService.getQuote().then(quote => this.quote = quote);
}
}
// #enddocregion component
@@ -0,0 +1,32 @@
import { Injectable } from '@angular/core';
const quotes = [
'Always do right. This will gratify some people and astonish the rest.',
'I have never let my schooling interfere with my education.',
'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.',
'Whenever you find yourself on the side of the majority, it is time to pause and reflect.',
'If you tell the truth, you don\'t have to remember anything.',
'Clothes make the man. Naked people have little or no influence on society.',
'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.',
'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.',
'The man who does not read good books has no advantage over the man who cannot read them.',
'Get your facts first, and then you can distort them as much as you please.',
];
@Injectable()
export class TwainService {
private next = 0;
// Imaginary todo: get quotes from a remote quote service
// returns quote after delay simulating server latency
getQuote(): Promise<string> {
return new Promise(resolve => {
setTimeout( () => resolve(this.nextQuote()), 500 );
});
}
private nextQuote() {
if (this.next === quotes.length) { this.next = 0; }
return quotes[ this.next++ ];
}
}
@@ -1,4 +0,0 @@
export class Quote {
id: number;
quote: string;
}
@@ -1,93 +0,0 @@
// #docplaster
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
// #docregion import-marbles
import { cold, getTestScheduler } from 'jasmine-marbles';
// #enddocregion import-marbles
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { last } from 'rxjs/operators';
import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
describe('TwainComponent (marbles)', () => {
let component: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let getQuoteSpy: jasmine.Spy;
let quoteEl: HTMLElement;
let testQuote: string;
// Helper function to get the error message element value
// An *ngIf keeps it out of the DOM until there is an error
const errorMessage = () => {
const el = fixture.nativeElement.querySelector('.error');
return el ? el.textContent : null;
};
beforeEach(() => {
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
getQuoteSpy = twainService.getQuote;
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
testQuote = 'Test Quote';
});
// A synchronous test that simulates async behavior
// #docregion get-quote-test
it('should show quote after getQuote (marbles)', () => {
// observable test quote value and complete(), after delay
// #docregion test-quote-marbles
const q$ = cold('---x|', { x: testQuote });
// #enddocregion test-quote-marbles
getQuoteSpy.and.returnValue( q$ );
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
// #docregion test-scheduler-flush
getTestScheduler().flush(); // flush the observables
// #enddocregion test-scheduler-flush
fixture.detectChanges(); // update view
expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
expect(errorMessage()).toBeNull('should not show error');
});
// #enddocregion get-quote-test
// Still need fakeAsync() because of component's setTimeout()
// #docregion error-test
it('should display error when TwainService fails', fakeAsync(() => {
// observable error after delay
// #docregion error-marbles
const q$ = cold('---#|', null, new Error('TwainService test failure'));
// #enddocregion error-marbles
getQuoteSpy.and.returnValue( q$ );
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
getTestScheduler().flush(); // flush the observables
tick(); // component shows error after a setTimeout()
fixture.detectChanges(); // update error message
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
// #enddocregion error-test
});
@@ -1,184 +0,0 @@
// #docplaster
import { async, fakeAsync, ComponentFixture, TestBed, tick } from '@angular/core/testing';
import { asyncData, asyncError } from '../../testing';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { last } from 'rxjs/operators';
import { TwainService } from './twain.service';
import { TwainComponent } from './twain.component';
describe('TwainComponent', () => {
let component: TwainComponent;
let fixture: ComponentFixture<TwainComponent>;
let getQuoteSpy: jasmine.Spy;
let quoteEl: HTMLElement;
let testQuote: string;
// Helper function to get the error message element value
// An *ngIf keeps it out of the DOM until there is an error
const errorMessage = () => {
const el = fixture.nativeElement.querySelector('.error');
return el ? el.textContent : null;
};
// #docregion setup
beforeEach(() => {
testQuote = 'Test Quote';
// #docregion spy
// Create a fake TwainService object with a `getQuote()` spy
const twainService = jasmine.createSpyObj('TwainService', ['getQuote']);
// Make the spy return a synchronous Observable with the test data
getQuoteSpy = twainService.getQuote.and.returnValue( of(testQuote) );
// #enddocregion spy
TestBed.configureTestingModule({
declarations: [ TwainComponent ],
providers: [
{ provide: TwainService, useValue: twainService }
]
});
fixture = TestBed.createComponent(TwainComponent);
component = fixture.componentInstance;
quoteEl = fixture.nativeElement.querySelector('.twain');
});
// #enddocregion setup
describe('when test with synchronous observable', () => {
it('should not show quote before OnInit', () => {
expect(quoteEl.textContent).toBe('', 'nothing displayed');
expect(errorMessage()).toBeNull('should not show error element');
expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
});
// The quote would not be immediately available if the service were truly async.
// #docregion sync-test
it('should show quote after component initialized', () => {
fixture.detectChanges(); // onInit()
// sync spy result shows testQuote immediately after init
expect(quoteEl.textContent).toBe(testQuote);
expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});
// #enddocregion sync-test
// The error would not be immediately available if the service were truly async.
// Use `fakeAsync` because the component error calls `setTimeout`
// #docregion error-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an error observable
getQuoteSpy.and.returnValue(
new ErrorObservable('TwainService test failure'));
fixture.detectChanges(); // onInit()
// sync spy errors immediately after init
tick(); // flush the component's setTimeout()
fixture.detectChanges(); // update errorMessage within setTimeout()
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
// #enddocregion error-test
});
describe('when test with asynchronous observable', () => {
beforeEach(() => {
// #docregion async-setup
// Simulate delayed observable values with the `asyncData()` helper
getQuoteSpy.and.returnValue(asyncData(testQuote));
// #enddocregion async-setup
});
it('should not show quote before OnInit', () => {
expect(quoteEl.textContent).toBe('', 'nothing displayed');
expect(errorMessage()).toBeNull('should not show error element');
expect(getQuoteSpy.calls.any()).toBe(false, 'getQuote not yet called');
});
it('should still not show quote after component initialized', () => {
fixture.detectChanges();
// getQuote service is async => still has not returned with quote
// so should show the start value, '...'
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
expect(errorMessage()).toBeNull('should not show error');
expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called');
});
// #docregion fake-async-test
it('should show quote after getQuote (fakeAsync)', fakeAsync(() => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
tick(); // flush the observable to get the quote
fixture.detectChanges(); // update view
expect(quoteEl.textContent).toBe(testQuote, 'should show quote');
expect(errorMessage()).toBeNull('should not show error');
}));
// #enddocregion fake-async-test
// #docregion async-test
it('should show quote after getQuote (async)', async(() => {
fixture.detectChanges(); // ngOnInit()
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
fixture.whenStable().then(() => { // wait for async getQuote
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
});
}));
// #enddocregion async-test
// #docregion quote-done-test
it('should show last quote (quote done)', (done: DoneFn) => {
fixture.detectChanges();
component.quote.pipe( last() ).subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
});
});
// #enddocregion quote-done-test
// #docregion spy-done-test
it('should show quote after getQuote (spy done)', (done: DoneFn) => {
fixture.detectChanges();
// the spy's most recent call returns the observable with the test quote
getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
fixture.detectChanges(); // update view with quote
expect(quoteEl.textContent).toBe(testQuote);
expect(errorMessage()).toBeNull('should not show error');
done();
});
});
// #enddocregion spy-done-test
// #docregion async-error-test
it('should display error when TwainService fails', fakeAsync(() => {
// tell spy to return an async error observable
getQuoteSpy.and.returnValue(asyncError<string>('TwainService test failure'));
fixture.detectChanges();
tick(); // component shows error after a setTimeout()
fixture.detectChanges(); // update error message
expect(errorMessage()).toMatch(/test failure/, 'should display error');
expect(quoteEl.textContent).toBe('...', 'should show placeholder');
}));
// #enddocregion async-error-test
});
});
@@ -1,49 +0,0 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { catchError, startWith } from 'rxjs/operators';
import { TwainService } from './twain.service';
// #docregion component
@Component({
selector: 'twain-quote',
// #docregion template
template: `
<p class="twain"><i>{{quote | async}}</i></p>
<button (click)="getQuote()">Next quote</button>
<p class="error" *ngIf="errorMessage">{{ errorMessage }}</p>`,
// #enddocregion template
styles: [
`.twain { font-style: italic; } .error { color: red; }`
]
})
export class TwainComponent implements OnInit {
errorMessage: string;
quote: Observable<string>;
constructor(private twainService: TwainService) {}
ngOnInit(): void {
this.getQuote();
}
// #docregion get-quote
getQuote() {
this.errorMessage = '';
this.quote = this.twainService.getQuote().pipe(
startWith('...'),
catchError( (err: any) => {
// Wait a turn because errorMessage already set once this turn
setTimeout(() => this.errorMessage = err.message || err.toString());
return of('...'); // reset message to placeholder
})
);
// #enddocregion get-quote
}
}
// #enddocregion component
@@ -1,15 +0,0 @@
import { Quote } from './quote';
export const QUOTES: Quote[] = [
'Always do right. This will gratify some people and astonish the rest.',
'I have never let my schooling interfere with my education.',
'Don\'t go around saying the world owes you a living. The world owes you nothing. It was here first.',
'Whenever you find yourself on the side of the majority, it is time to pause and reflect.',
'If you tell the truth, you don\'t have to remember anything.',
'Clothes make the man. Naked people have little or no influence on society.',
'It\'s not the size of the dog in the fight, it\'s the size of the fight in the dog.',
'Truth is stranger than fiction, but it is because Fiction is obliged to stick to possibilities; Truth isn\'t.',
'The man who does not read good books has no advantage over the man who cannot read them.',
'Get your facts first, and then you can distort them as much as you please.',
]
.map((q, i) => ({ id: i + 1, quote: q }));
@@ -1,47 +0,0 @@
// Mark Twain Quote service gets quotes from server
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { of } from 'rxjs/observable/of';
import { concat, map, retryWhen, switchMap, take, tap } from 'rxjs/operators';
import { Quote } from './quote';
@Injectable()
export class TwainService {
constructor(private http: HttpClient) { }
private nextId = 1;
getQuote(): Observable<string> {
return Observable.create(observer => observer.next(this.nextId++)).pipe(
// tap((id: number) => console.log(id)),
// tap((id: number) => { throw new Error('Simulated server error'); }),
switchMap((id: number) => this.http.get<Quote>(`api/quotes/${id}`)),
// tap((q : Quote) => console.log(q)),
map((q: Quote) => q.quote),
// `errors` is observable of http.get errors
retryWhen(errors => errors.pipe(
switchMap((error: HttpErrorResponse) => {
if (error.status === 404) {
// Queried for quote that doesn't exist.
this.nextId = 1; // retry with quote id:1
return of(null); // signal OK to retry
}
// Some other HTTP error.
console.error(error);
return new ErrorObservable('Cannot get Twain quotes from the server');
}),
take(2),
// If a second retry value, then didn't find id:1 and triggers the following error
concat(new ErrorObservable('There are no Twain quotes')) // didn't find id:1
))
);
}
}
@@ -1,66 +1,26 @@
// #docplaster
import { ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { UserService } from '../model/user.service';
import { UserService } from './model';
import { WelcomeComponent } from './welcome.component';
// #docregion mock-user-service
class MockUserService {
isLoggedIn = true;
user = { name: 'Test User'};
};
// #enddocregion mock-user-service
describe('WelcomeComponent (class only)', () => {
let comp: WelcomeComponent;
let userService: UserService;
// #docregion class-only-before-each
beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
{ provide: UserService, useClass: MockUserService }
]
});
// inject both the component and the dependent service.
comp = TestBed.get(WelcomeComponent);
userService = TestBed.get(UserService);
});
// #enddocregion class-only-before-each
// #docregion class-only-tests
it('should not have welcome message after construction', () => {
expect(comp.welcome).toBeUndefined();
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
comp.ngOnInit();
expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
userService.isLoggedIn = false;
comp.ngOnInit();
expect(comp.welcome).not.toContain(userService.user.name);
expect(comp.welcome).toContain('log in');
});
// #enddocregion class-only-tests
});
describe('WelcomeComponent', () => {
let comp: WelcomeComponent;
let fixture: ComponentFixture<WelcomeComponent>;
let componentUserService: UserService; // the actually injected service
let userService: UserService; // the TestBed injected service
let de: DebugElement; // the DebugElement with the welcome message
let el: HTMLElement; // the DOM element with the welcome message
// #docregion setup, user-service-stub
let userServiceStub: Partial<UserService>;
let userServiceStub: {
isLoggedIn: boolean;
user: { name: string}
};
// #enddocregion user-service-stub
// #docregion setup
beforeEach(() => {
// stub UserService for test purposes
// #docregion user-service-stub
@@ -97,7 +57,8 @@ describe('WelcomeComponent', () => {
// #enddocregion inject-from-testbed
// get the "welcome" element by CSS selector (e.g., by class name)
el = fixture.nativeElement.querySelector('.welcome');
de = fixture.debugElement.query(By.css('.welcome'));
el = de.nativeElement;
});
// #enddocregion setup
@@ -124,10 +85,12 @@ describe('WelcomeComponent', () => {
});
// #enddocregion tests
// #docregion inject-it
it('should inject the component\'s UserService instance',
inject([UserService], (service: UserService) => {
expect(service).toBe(componentUserService);
}));
// #enddocregion inject-it
it('TestBed and Component UserService should be the same', () => {
expect(userService === componentUserService).toBe(true);
@@ -1,20 +1,19 @@
// #docregion
import { Component, OnInit } from '@angular/core';
import { UserService } from '../model/user.service';
// #docregion component
import { UserService } from './model/user.service';
@Component({
selector: 'app-welcome',
template: '<h3 class="welcome"><i>{{welcome}}</i></h3>'
template: '<h3 class="welcome" ><i>{{welcome}}</i></h3>'
})
// #docregion class
export class WelcomeComponent implements OnInit {
welcome: string;
welcome = '-- not initialized yet --';
constructor(private userService: UserService) { }
ngOnInit(): void {
this.welcome = this.userService.isLoggedIn ?
'Welcome, ' + this.userService.user.name : 'Please log in.';
'Welcome, ' + this.userService.user.name :
'Please log in.';
}
}
// #enddocregion component, class
+26
View File
@@ -0,0 +1,26 @@
<!-- Run the bag source as an app -->
<!DOCTYPE html>
<html>
<head>
<base href="/">
<title>Specs Bag</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="styles.css">
<!-- Polyfills -->
<script src="node_modules/core-js/client/shim.min.js"></script>
<script src="node_modules/zone.js/dist/zone.js"></script>
<script src="node_modules/systemjs/dist/system.src.js"></script>
<script src="systemjs.config.js"></script>
<script>
System.import('app/bag/bag-main').catch(function(err){ console.error(err); });
</script>
</head>
<body>
<bag-comp>Loading ...</bag-comp>
</body>
</html>
@@ -1,29 +0,0 @@
// export for convenience.
export { ActivatedRoute } from '@angular/router';
// #docregion activated-route-stub
import { ReplaySubject } from 'rxjs/ReplaySubject';
import { convertToParamMap, ParamMap, Params } from '@angular/router';
/**
* An ActivateRoute test double with a `paramMap` observable.
* Use the `setParamMap()` method to add the next `paramMap` value.
*/
export class ActivatedRouteStub {
// Use a ReplaySubject to share previous values with subscribers
// and pump new values into the `paramMap` observable
private subject = new ReplaySubject<ParamMap>();
constructor(initialParams?: Params) {
this.setParamMap(initialParams);
}
/** The mock paramMap observable */
readonly paramMap = this.subject.asObservable();
/** Set the paramMap observables's next value */
setParamMap(params?: Params) {
this.subject.next(convertToParamMap(params));
};
}
// #enddocregion activated-route-stub
@@ -1,30 +0,0 @@
/*
* Mock async observables that return asynchronously.
* The observable either emits once and completes or errors.
*
* Must call `tick()` when test with `fakeAsync()`.
*
* THE FOLLOWING DON'T WORK
* Using `of().delay()` triggers TestBed errors;
* see https://github.com/angular/angular/issues/10127 .
*
* Using `asap` scheduler - as in `of(value, asap)` - doesn't work either.
*/
import { Observable } from 'rxjs/Observable';
import { defer } from 'rxjs/observable/defer';
// #docregion async-data
/** Create async observable that emits-once and completes
* after a JS engine turn */
export function asyncData<T>(data: T) {
return defer(() => Promise.resolve(data));
}
// #enddocregion async-data
// #docregion async-error
/** Create async observable error that errors
* after a JS engine turn */
export function asyncError<T>(errorObject: any) {
return defer(() => Promise.reject(errorObject));
}
// #enddocregion async-error
@@ -1,10 +1,8 @@
import { DebugElement } from '@angular/core';
import { tick, ComponentFixture } from '@angular/core/testing';
export * from './async-observable-helpers';
export * from './activated-route-stub';
export * from './jasmine-matchers';
export * from './router-link-directive-stub';
export * from './router-stubs';
///// Short utilities /////
@@ -1,30 +0,0 @@
import { Directive, Input } from '@angular/core';
// export for convenience.
export { RouterLink} from '@angular/router';
/* tslint:disable:directive-class-suffix */
// #docregion router-link
@Directive({
selector: '[routerLink]',
host: { '(click)': 'onClick()' }
})
export class RouterLinkDirectiveStub {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
onClick() {
this.navigatedTo = this.linkParams;
}
}
// #enddocregion router-link
/// Dummy module to satisfy Angular Language service. Never used.
import { NgModule } from '@angular/core';
@NgModule({
declarations: [
RouterLinkDirectiveStub
]
})
export class RouterStubsModule {}
@@ -0,0 +1,58 @@
// export for convenience.
export { ActivatedRoute, Router, RouterLink, RouterOutlet} from '@angular/router';
import { Component, Directive, Injectable, Input } from '@angular/core';
import { NavigationExtras } from '@angular/router';
// #docregion router-link
@Directive({
selector: '[routerLink]',
host: {
'(click)': 'onClick()'
}
})
export class RouterLinkStubDirective {
@Input('routerLink') linkParams: any;
navigatedTo: any = null;
onClick() {
this.navigatedTo = this.linkParams;
}
}
// #enddocregion router-link
@Component({selector: 'router-outlet', template: ''})
export class RouterOutletStubComponent { }
@Injectable()
export class RouterStub {
navigate(commands: any[], extras?: NavigationExtras) { }
}
// Only implements params and part of snapshot.paramMap
// #docregion activated-route-stub
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { convertToParamMap, ParamMap } from '@angular/router';
@Injectable()
export class ActivatedRouteStub {
// ActivatedRoute.paramMap is Observable
private subject = new BehaviorSubject(convertToParamMap(this.testParamMap));
paramMap = this.subject.asObservable();
// Test parameters
private _testParamMap: ParamMap;
get testParamMap() { return this._testParamMap; }
set testParamMap(params: {}) {
this._testParamMap = convertToParamMap(params);
this.subject.next(this._testParamMap);
}
// ActivatedRoute.snapshot.paramMap
get snapshot() {
return { paramMap: this.testParamMap };
}
}
// #enddocregion activated-route-stub

Some files were not shown because too many files have changed in this diff Show More