Compare commits

...

260 Commits

Author SHA1 Message Date
aabe16c08c fix(bazel): ng_package includes transitive .d.ts and flatModuleMetadata (#22499)
Fixes #22419

PR Close #22499
2018-02-28 13:29:56 -08:00
b6c941053e feat(bazel): ng_package adds package.json props (#22499)
We now add the 'main', 'module', 'es2015', and 'typings' properties,
pointing to where the packaging tool lays them out.

Fixes #22416

PR Close #22499
2018-02-28 13:29:56 -08:00
c82cef8bc6 docs: fix dynamic component loader example (#22181)
closes #21903

PR Close #22181
2018-02-28 10:46:37 -08:00
f8749bfb70 fix(core): export inject() from @angular/core (#22389)
inject() supports the ngInjectableDef-based configuration of the injector
(otherwise known as tree-shakeable services). It was missing from the
exported API of @angular/core, this PR adds it.

The test added here is correct in theory, but may pass accidentally due
to the decorator side-effect replacing the inject() call at runtime. An
upcoming compiler PR will strip reified decorators from the output
entirely.

Fixes #22388

PR Close #22389
2018-02-28 10:44:37 -08:00
7d65356ae3 build(aio): add @usageNotes tag def for API docs (#22401)
PR Close #22401
2018-02-28 10:43:27 -08:00
11f30fc351 style(aio): add newline between test blocks (#22401)
PR Close #22401
2018-02-28 10:43:27 -08:00
b107131f8a build(aio): split the description property in API docs (#22401)
* The first paragraph is now split off into the `shortDescription` property.
* Usage of `howToUse` and `whatItDoes` have been updated.
* The "Overview" heading for class is removed as it is self-evident
* The original horizontal rule styling below the main heading is removed as not part of the new design

Closes #22385

PR Close #22401
2018-02-28 10:43:27 -08:00
11264e2174 fix(aio): remove heading border (#22401)
PR Close #22401
2018-02-28 10:43:27 -08:00
b924ce3a62 build(aio): add processor to migrate legacy tags @whatItDoes and @howToUse (#22401)
See https://github.com/angular/angular/issues/22135#issuecomment-367632372

PR Close #22401
2018-02-28 10:43:27 -08:00
e75f0cee18 build(aio): deprecate @howToUse and @whatItDoes tags (#22401)
See https://github.com/angular/angular/issues/22135#issuecomment-367632372

PR Close #22401
2018-02-28 10:43:27 -08:00
8c358844dd feat(ivy): support OnPush change detection (#22417)
PR Close #22417
2018-02-28 10:42:11 -08:00
e454c5a98e refactor(ivy): store creationMode in LView.flags (#22417)
PR Close #22417
2018-02-28 10:42:11 -08:00
930ecacd86 build: update ts-api-guardian version (#22402)
PR Close #22402
2018-02-28 09:29:29 -08:00
5170ffe844 build: update api golden files (#22402)
`ts-api-guardion` has been updated to accept new TypeScript syntax

PR Close #22402
2018-02-28 09:29:29 -08:00
e95b61d42a docs: fix community tab in GitHub by copying CoC 2018-02-27 19:02:30 -08:00
8a85888773 fix(upgrade): correctly destroy nested downgraded component (#22400)
Previously, when a downgraded component was destroyed in a way that did
not trigger the `$destroy` event on the element (e.g. when a parent
element was removed from the DOM by Angular, not AngularJS), the
`ComponentRef` was not destroyed and unregistered.
This commit fixes it by listening for the `$destroy` event on both the
element and the scope.

Fixes #22392

PR Close #22400
2018-02-27 18:41:02 -08:00
83b32a0a0a build(aio): render heading anchors on the right
This approach simplifies the styling needed considerably.
Previously, we had to make room on the left for heading that
are in visual containers. Also we had to apply a `float:right`
when on narrow screens as the gutter not available then.
This float didn't render nicely if the heading text was longer
than could be rendered on a single line.

Closes #22131
2018-02-27 18:32:32 -08:00
8d34364ff5 ci: update github bot config (#22453)
PR Close #22453
2018-02-27 15:18:36 -08:00
142117f6bb fix(aio): do not show Properties/Methods sections if only internal members (#22471)
PR Close #22471
2018-02-27 15:17:22 -08:00
79656e7f96 docs(aio): add Observable and Rx docs (#21423)
PR Close #21423
2018-02-27 11:24:31 -08:00
d100f1b187 build(aio): fix API docs breadcrumbs (#22446)
This also prevents some extra `<a>` elements inserted by the browser's
trying to fix the HTML structure, which also fixes the `.header-link`
added in ToC.

Fixes #22387
Closes #22437

PR Close #22446
2018-02-27 07:08:07 -08:00
4bd3e5f92f build(aio): do not render "constructor()" heading in API docs (#22380)
Closes #22363

PR Close #22380
2018-02-27 07:07:16 -08:00
3f3be429c9 style(aio): updated padding-right for the .alert class in _heading-anchors.scss (#22431)
The h3 element is overflowing over its surrounding div element. Modified padding-right to align consistently with the remainder of div contents.

fixes: #22407

PR Close #22431
2018-02-26 17:52:29 -08:00
7e4c13f2de style(aio): added padding-left to h3 in _subsection.scss (#22431)
The h3 element is overflowing over its surrounding div element. Modified padding-left to align consistently with the remainder of div contents.

fixes: #22407

PR Close #22431
2018-02-26 17:52:29 -08:00
82a791223c docs: update i18n guide for projects that don't use the cli (#21767)
PR Close #21767
2018-02-26 17:51:58 -08:00
ef99126aea test(aio): move forgotten e2e test to the correct folder (#22445)
PR Close #22445
2018-02-26 17:50:42 -08:00
c10c831b8e build(aio): render subclasses + see-also block in the main flow (#22445)
Closes #22386

PR Close #22445
2018-02-26 17:50:42 -08:00
40ba009e25 fix(platform-server): generate correct stylings for camel case names (#22263)
* Add correct mapping from camel case to kebab case for CSS style
names
* Remove internal CSS methods in favor of native Domino APIs

Fixes #19235

PR Close #22263
2018-02-26 17:46:21 -08:00
d3827a0017 feat(platform-server): bump Domino to v2.0 (#22411)
BREAKING CHANGE:

* Bump the dependency on Domino to 2.0 to resolve issues with
  namespacing

PR Close #22411
2018-02-26 14:55:52 -08:00
a7e1f236ff docs: testing guide for CLI (#20697)
- updates tests
- heavy prose revisions
- uses HttpClient (with angular-in-memory-web-api)
- test HeroService using `HttpClientTestingModule`
- scrub away most By.CSS
- fake async observable with `asyncData()`
- extensive Twain work
- different take on retryWhen
- remove app barrels (& systemjs.extras) which troubled plunker/systemjs
- add dummy export const to hero.ts (plunkr/systemjs fails w/o it)
- shrink and re-organize TOC
- add marble testing package and tests
- demonstrate the "no beforeEach()" test coding style
- add section on Http service testing
- prepare for stackblitz
- confirm works in plunker except excluded marble test
- add tests for avoidFile class feature of CodeExampleComponent

PR Close #20697
2018-02-26 13:40:23 -08:00
1f599818bd build(aio): fix rendering of callable and constructable interface members (#22428)
Closes #22136

PR Close #22428
2018-02-26 13:35:15 -08:00
5a32d7e36f build(aio): render default value for optional parameters (#22435)
Closes #22134

PR Close #22435
2018-02-26 13:34:38 -08:00
1ea41d48d3 build(aio): render whether parameters are optional (#22435)
Closes #22134

PR Close #22435
2018-02-26 13:34:38 -08:00
25a43041d2 build(aio): group API members by type in overview (#22438)
Now the overview groups the members in the following order:

* static properties
* static methods
* constructor
* instance properties
* instance members

Closes #22132

PR Close #22438
2018-02-26 13:34:04 -08:00
c593d69ce7 docs(aio): fix doc typo referring to httpOptions (#22456)
The variable name mention should match the actual tutorial code.

PR Close #22456
2018-02-26 13:32:44 -08:00
13ab91e05d fix(upgrade): fix empty transclusion content with AngularJS@>=1.5.8 (#22167)
The function provided by `ngUpgrade` as `parentBoundTranscludeFn` when
upgrading a component with transclusion, will break in AngularJS v1.5.8+
if no transclusion content is provided. The reason is that AngularJS
will try to destroy the transclusion scope (which would not be needed
any more). But since the transcluded content comes from Angular, not
AngularJS, there is no transclusion scope to destroy.
This commit fixes it by providing a dummy scope object with a no-op
`$destroy()` method.

Fixes #22175

PR Close #22167
2018-02-25 10:06:14 -08:00
f089bf5333 fix(upgrade): correctly handle = bindings in @angular/upgrade (#22167)
Previously, having a `=` binding on an upgraded components would result
in setting the corresponding property to an EventEmitter function. This
should only happen for `&` bindings.
This commit rstrores the correct behavior.

Note:
The issue was only present in the dynamic version of `ngUpgrade`. The
static version worked as expected.
The error did not show up in tests, because in AngularJS v1.5.x a
function would be serialized to an empty string in interpolations, thus
making them indistinguishable from uninitialized properties (in the
view). The serialization behavior changed in AngularJS v1.6.x, making
the errors visible.

PR Close #22167
2018-02-25 10:06:14 -08:00
8e1e040f72 test(upgrade): run tests against multiple AngularJS versions (#22167)
Fixes #19332

PR Close #22167
2018-02-25 10:06:14 -08:00
28240625e6 refactor(upgrade): use correct paths for imports (#22167)
`packages/upgrade/static/src` is anymlink to `packages/upgrade/src`.
Still, using the correct paths (e.g. using
`@angular/upgrade/static/src/...` for `@angula/upgrade/static` specs
ensures that the module loader (e.g. SystemJS) can map the imports to
the same instances.

PR Close #22167
2018-02-25 10:06:14 -08:00
0d248079ba test(platform-browser): remove stray debugger statement (#22167)
PR Close #22167
2018-02-25 10:06:14 -08:00
a4032296cc Revert "fix(router): fix URL serialization so special characters are only encoded where needed (#22337)"
This reverts commit 094666da17.
2018-02-23 18:12:40 -08:00
4180912538 feat(common): export functions to format numbers, percents, currencies & dates (#22423)
The utility functions `formatNumber`, `formatPercent`, `formatCurrency`, and `formatDate` used by the number, percent, currency and date pipes are now available for developers who want to use them outside of templates.

Fixes #20536

PR Close #22423
2018-02-23 15:27:10 -08:00
094666da17 fix(router): fix URL serialization so special characters are only encoded where needed (#22337)
Fixes: #10280

This change brings Angular largely in line with how AngularJS previously serialized URLs. This is based on [RFC 3986](http://tools.ietf.org/html/rfc3986) and resolves issues such as the above #10280 where URLs could be parsed, re-serialized, then parsed again producing a different result on the second parsing.

Adjustments to be aware of in this commit:

* Query strings will now serialize with decoded slash (`/`) and question mark (`?`)
* URI fragments will now serialize the same as query strings, but hash sign (`#`) will also appear decoded
* In the URI path or segments (portion prior to query string and/or fragment), the plus sign (`+`) and ampersand (`&`) will appear decoded
* In the URL path or segments, parentheses values (`(` and `)`) will now appear percent encoded as `%28` and `%29` respectively
* In the URL path or segments, semicolons will be encoded in their percent encoding `%3B`

NOTE: Parentheses and semicolons denoting auxillary routes or matrix params will still appear in their decoded form -- only parentheses and semicolons used as values in a segment or key/value pair for matrix params will be encoded.

While these changes are not considered breaking because applications should be decoding URLs and key/value pairs, it is possible that some unit tests will break if comparing hard-coded URLs in tests since that hard coded string will represent the old encoding. Therefore we are releasing this fix in the upcoming Angular v6 rather than adding it to a patch for v5.

PR Close #22337
2018-02-23 13:20:51 -08:00
3a809cb431 build: add support for the "merge-assistance" label in merge-pr (#22414)
fixes #22256

PR Close #22414
2018-02-23 12:58:30 -08:00
b43b164a61 feat(bazel): add an ng_package rule (#22221)
This produces a directory following the Angular Package layout spec.

Includes integration test coverage by making a minimal ng_package in integration/bazel.
Unit tests verify the content of the @angular/core and @angular/common packages.

This doesn't totally match our current output, but is good enough to unblock some
early adopters.

It re-uses logic from the rollup_bundle rule in rules_nodejs. It should also
eventually have the .pack and .publish secondary targets like npm_package rule.

PR Close #22221
2018-02-23 11:19:04 -08:00
1dcbc12fd3 docs(aio): Essential JS 2 url updated (#19739)
PR Close #19739
2018-02-23 11:18:11 -08:00
ae2e85e8ef docs(aio): Essential JS 2 UI Components. (#19739)
PR Close #19739
2018-02-23 11:18:11 -08:00
aad431642a refactor(ivy): rename componentRefresh to directiveRefresh (#22395)
PR Close #22395
2018-02-23 09:42:08 -08:00
a81d599bfc ci: don't use bazel git_repository rule (#22406)
It's currently broken on CircleCI because of a TLS change made by GitHub.
This is okay as a permanent change, we don't really want bazel to fetch a full git history.

Fixes #22405

PR Close #22406
2018-02-23 09:41:10 -08:00
7effb0016c fix(ivy): ngOnChanges to receive SimpleChanges with non minified property names as keys (#22352)
PR Close #22352
2018-02-22 17:48:52 -08:00
f791862e52 build: add release helper scripts (#22378)
PR Close #22378
2018-02-22 16:12:50 -08:00
b2f366b3b7 fix(animations): only use the WA-polyfill alongside AnimationBuilder (#22143)
This patch removes the need to include the Web Animations API Polyfill
(web-animations-js) as a dependency. Angular will now fallback to using
CSS Keyframes in the event that `element.animate` is no longer supported
by the browser.

In the event that an application does use `AnimationBuilder` then the
web-animations-js polyfill is required to enable programmatic,
position-based access to an animation.

Closes #17496

PR Close #22143
2018-02-22 16:07:53 -08:00
9eecb0b27f docs: fix deployment sample path (#22048)
PR Close #22048
2018-02-22 13:40:57 -08:00
45eff4cc65 fix(router): don't mutate route configs (#22358)
Fixes #22203

PR Close #22358
2018-02-22 13:35:38 -08:00
b3ffeaa22b fix(ivy): OnDestroy hook should not be called twice for a directive on an element (#22350)
PR Close #22350
2018-02-22 13:35:17 -08:00
f194d00366 build: disable bazel-out symlink (#22375)
It causes headaches on MacOS High Sierra, see https://github.com/bazelbuild/bazel/issues/4603

PR Close #22375
2018-02-22 13:31:59 -08:00
b7544cccc6 feat(core): support metadata reflection for native class types (#22356)
closes #21731

PR Close #22356
2018-02-22 13:22:03 -08:00
894b098eb3 test(ivy): add missing tests on directive lifecycle hooks (#22368)
PR Close #22368
2018-02-22 11:21:15 -08:00
022ad4a420 docs: fix ngmodules-jsmodules pre-req (#22316)
closes #22157

PR Close #22316
2018-02-22 11:20:47 -08:00
a4f9e8180b docs: edit styleguide recommendation on components as elements (#22074)
Change recommendation on using attributes for components since there are use cases including the use of <button mat-button> in MD

Closes #19401.

PR Close #22074
2018-02-22 11:20:21 -08:00
e86b64b620 feat(forms): allow markAsPending to emit events (#20212)
closes #17958

BREAKING CHANGE:
- `AbstractControl#statusChanges` now emits an event of `'PENDING'` when you call `AbstractControl#markAsPending`
- Previously it did not emit an event when you called `markAsPending`
- To migrate you would need to ensure that if you are filtering or checking events from `statusChanges` that you account for the new event when calling `markAsPending`

PR Close #20212
2018-02-22 11:15:33 -08:00
90e9c59e23 Revert "feat(core): support metadata reflection for native class types (#22356)"
This reverts commit 5c89d6bffa.
2018-02-22 10:26:06 -08:00
ca06af40f4 build: allow passing node options to ngc. (#22245)
PR Close #22245
2018-02-22 10:20:04 -08:00
6091a954cc docs: add changelog for 6.0.0-beta.5 2018-02-21 17:15:41 -08:00
d27fca9301 release: cut the 6.0.0-beta.5 release 2018-02-21 16:57:40 -08:00
5c89d6bffa feat(core): support metadata reflection for native class types (#22356)
closes #21731

PR Close #22356
2018-02-21 16:09:27 -08:00
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
a7ebf5aadd fix(core): properly handle function without prototype in reflector (#22284)
closes #19978

PR Close #22284
2018-02-21 14:52:04 -08:00
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
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
9e6268ba59 docs(http): fix a typo in code comment (#22327)
PR Close #22327
2018-02-21 11:06:06 -08:00
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
1c1cbba04b docs: add ngStyle to cheat sheet (#22070)
PR Close #22070
2018-02-20 16:08:15 -08:00
3b692a55a7 docs(aio): fix incorrect quote mark usage (#22335)
PR Close #22335
2018-02-20 15:42:55 -08:00
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
b5ca275590 docs(aio): Fix name of component (#22332)
PR Close #22332
2018-02-20 15:41:35 -08:00
519f022b02 docs(aio): update installed mobile tool list (#22331)
PR Close #22331
2018-02-20 15:41:15 -08:00
236a9320df build: update tsickle dep from compiler-cli (#22295)
PR Close #22295
2018-02-20 15:40:44 -08:00
28ac24444f fix(compiler-cli): add missing entry point to package, update tsickle (#22295)
PR Close #22295
2018-02-20 15:40:44 -08:00
99909bbf2c feat(ivy): generate pipe references and definitions (#22034)
PR Close #22034
2018-02-20 13:58:03 -08:00
ee60bb5b36 fix(ivy): pureFunction8 should update the right bindings (#22313)
PR Close #22313
2018-02-20 11:36:50 -08:00
f6120c09e7 docs(aio): add Nx and Angular Enterprise Playbook to resources (#22321)
PR Close #22321
2018-02-20 10:09:33 -08:00
e2bdef4cf6 test(language-service): fix minor typos (#21372)
PR Close #21372
2018-02-20 10:08:55 -08:00
8115edc82f fix(common): then and else template might be set to null (#22298)
PR Close #22298
2018-02-18 19:25:28 -08:00
a8b5465e24 fix(ivy): update master with renamings (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
9ce495b3d8 refactor(ivy): simplify interpolation instructions (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
d40263447d refactor(ivy): move get functions next to their underlying variable (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
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
c9ebd60435 refactor(ivy): interpolatiom instructions do not support NO_CHANGE at input. (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
5a14e2238f refactor(ivy): split the memory instruction into store and load (#22268)
PR Close #22268
2018-02-18 18:48:41 -08:00
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
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
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
c30d329faa fix(ivy): fix merge errors (master is broken) (#22291)
PR Close #22291
2018-02-18 15:03:39 -08:00
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
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
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
49082d7ab2 feat(ivy): support host attributes (#22213)
PR Close #22213
2018-02-18 13:22:38 -08:00
6b627f67db test(ivy): add missing host listener and host attribute binding tests (#22213)
PR Close #22213
2018-02-18 13:22:38 -08:00
5c320b4c2a test(ivy): add missing host binding and query tests (#22213)
PR Close #22213
2018-02-18 13:22:34 -08:00
ac2b04a5ab test(ivy): Add small_app spec for sprint #3 (#22018)
PR Close #22018
2018-02-18 13:18:54 -08:00
a63b764b54 test(ivy): move compiler canonical specs into a single directory (#22018)
PR Close #22018
2018-02-18 13:18:54 -08:00
2654357c72 docs: update BAZEL.md crosstool error instructions (#22018)
PR Close #22018
2018-02-18 13:18:54 -08:00
4ec40c6ab2 fix(aio): improve announcement-bar layout with wide logos (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
80d424798e docs(aio): added ngconf announcement (#22272)
PR Close #22272
2018-02-18 13:16:30 -08:00
7fa2d4b503 fix: merge-pr script (#22290)
PR Close #22290
2018-02-18 13:13:29 -08:00
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
f693be3996 feat(ivy): add pureFunction0 instruction (#22214)
PR Close #22214
2018-02-16 18:03:55 -08:00
a73d5308e0 refactor(ivy): rename objectLiteral to pureFn to prep for pipes (#22214)
PR Close #22214
2018-02-16 18:03:55 -08:00
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
884de18cba docs: replace plnkr with StackBlitz (#20365)
PR Close #20365
2018-02-16 15:12:10 -08:00
dfa2fb95d5 test(ivy): clean the imported renderer2 (#22255)
PR Close #22255
2018-02-16 15:11:23 -08:00
2639b4bffb fix(common): correct mapping of Observable methods (#20518)
fixes #20516
PR Close #20518
2018-02-16 15:10:31 -08:00
978f97cc59 test(aio): increase docs-test timeouts to prevent flakes on Travis (#22261)
PR Close #22261
2018-02-16 14:46:23 -08:00
f1a063298e feat(core): set preserveWhitespaces to false by default (#22046)
Fixes #22027

PR Close #22046
2018-02-16 09:06:14 -08:00
d241532488 docs(ivy): add a note about exporting top level variables (#22234)
PR Close #22234
2018-02-15 16:16:40 -08:00
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
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
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
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
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
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
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
020338230f style: fix typos boostrap to bootstrap (#21917)
PR Close #21917
2018-02-15 09:54:00 -08:00
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
7078fbffb4 fix(aio): improve printing styles (#19651)
printfix

PR Close #19651
2018-02-15 09:52:32 -08:00
0aa9b46b79 Revert "build: allow bazel build ... (#22168)"
This reverts commit 265ac8a106.
2018-02-15 03:28:35 -08:00
831592c381 Revert "style: fix typos boostrap to bootstrap (#21917)"
This reverts commit 363498b6b4.
2018-02-14 22:56:44 -05:00
f628797d91 Revert "refactor(ivy): assertion (#22189)"
This reverts commit 0b683123d2.
2018-02-14 22:56:11 -05:00
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
ba9cd5bbc4 Revert "fix(aio): improve printing styles (#19651)"
This reverts commit b54ad053f9.
2018-02-14 22:55:24 -05:00
b54ad053f9 fix(aio): improve printing styles (#19651)
printfix

PR Close #19651
2018-02-14 18:49:58 -05:00
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
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
363498b6b4 style: fix typos boostrap to bootstrap (#21917)
PR Close #21917
2018-02-14 18:21:52 -05:00
a1bb56f739 docs: fix changelog errors (#22226)
PR Close #22226
2018-02-14 18:18:22 -05:00
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
1d571b299d feat(platform-browser): fix #19604, can config hammerOptions (#21979)
PR Close #21979
2018-02-14 15:02:58 -05:00
3a0b5a928c docs(aio): fix extraneous divs (#22069)
PR Close #22069
2018-02-14 15:02:36 -05:00
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
fa7d8907d0 docs: add changelog for 6.0.0-beta.4 2018-02-13 21:35:43 -08:00
0220ce7002 docs: add changelog for 5.2.5 2018-02-13 21:34:04 -08:00
3bd0b2ab28 release: cut the 6.0.0-beta.4 release 2018-02-13 21:31:14 -08:00
a589ca0adb test(ivy): clean up canonical spec (#22188)
PR Close #22188
2018-02-13 13:04:43 -08:00
72f8abd7b3 fix(compiler): make unary plus operator consistent to JavaScript (#22154)
fixes #22089

PR Close #22154
2018-02-13 13:04:30 -08:00
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
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
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
b75cf3f70b ci: update ngbot config file (#22173)
Fixes #22053
PR Close #22173
2018-02-13 10:26:06 -08:00
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
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
2de0d4c1db perf(ivy): use official build optimizer rollup plugin in int test (#22121)
PR Close #22121
2018-02-13 10:25:37 -08:00
5e4af7c550 fix(ivy): o2+ should work with multiple template instances (#22075)
Closes #22075
2018-02-13 10:24:41 -08:00
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
eb48750705 docs(aio): fix typo in "preserveWhitespaces" example (#22182)
Fixes #22147

PR Close #22182
2018-02-12 15:57:42 -08:00
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
b333919722 build(bazel): allow ng_modules to elide .ngsummary.closure.js files (#22107)
PR Close #22107
2018-02-12 15:57:17 -08:00
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
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
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
0a1a397cd7 fix(platform-browser): add @Injectable where it was missing (#22005)
PR Close #22005
2018-02-12 14:34:59 -08:00
7f9b1b78f6 docs(aio): add angular-playground to resources (#22042)
PR Close #22042
2018-02-12 14:30:58 -08:00
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
88bec238ac fix(aio): remove broken span closing tag (#22146)
PR Close #22146
2018-02-12 10:01:10 -08:00
62e7b9da1e refactor(ivy): code simplification (#22082)
PR Close #22082
2018-02-12 10:00:56 -08:00
61341b2791 refactor(ivy): generatePropertyAliases (#22082)
PR Close #22082
2018-02-12 10:00:56 -08:00
92a5876f51 refactor(router): move activation to private method (#22033)
PR Close #22033
2018-02-12 10:00:36 -08:00
a57df4ee20 docs(aio): put structural directives back in the nav (#21856)
PR Close #21856
2018-02-12 10:00:14 -08:00
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
7e9b120452 build: update to latest bazel rules (#22127)
PR Close #22127
2018-02-09 17:21:54 -08:00
b081dfe705 fix(bazel): allow TS to read ambient typings (#21876)
Same fix as e70d7a2a7c
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
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
c878d55397 docs: add VSCode interaction issue to bazel docs (#22128)
PR Close #22128
2018-02-09 17:13:06 -08:00
263a2eca88 build(bazel): allow ng_modules to elide .ngsummary.closure.js files (#22107)
PR Close #22107
2018-02-09 16:07:49 -08:00
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
0b2f7d13d0 fix(common): regenerate i18n locale data files (#21783)
PR Close #21783
2018-02-09 14:42:23 -08:00
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
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
c3484450b8 docs: fix typo in http.md (#22058)
PR Close #22058
2018-02-09 13:10:23 -08:00
fbef94a8ee feat(aio): enable data driven homepage announcements (#22043)
PR Close #22043
2018-02-09 13:10:11 -08:00
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
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
bc1e22922a docs(aio): several fix for ngmodule guides (#21517)
PR Close #21517
2018-02-09 13:03:47 -08:00
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
0b1f5d2127 Revert "docs(common): add HttpParamsOptions to the public API (#20332)"
This reverts commit a9545aba4d.
2018-02-08 14:37:27 -08:00
dcf64a0d01 fix(bazel): improve error message for missing assets (#22096)
fixes #22095

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

PR Close #20332
2018-02-08 09:44:36 -08:00
d9ae70c699 test(ivy): normalize template names in canonical spec (#21815)
PR Close #21815
2018-02-08 08:55:40 -08:00
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
3f5a3d6ea1 refactor(ivy): add internal isProceduralRenderer() (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
10a014d89e refactor(ivy): prefix viewStart & viewEnd with embedded (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
8feb8e5408 refactor(ivy): use long instruction format in tests (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
16dada28f5 docs(ivy): Simplify & dedup API docs for canInsertNativeNode (#22055)
PR Close #22055
2018-02-07 17:03:25 -08:00
67cf7128ae docs(aio): remove ngATL banner from homepage (#22060)
Closes #22029

PR Close #22060
2018-02-07 16:10:17 -08:00
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
83d43ac850 docs(aio): remove lifecycle hooks img (#21425)
PR Close #21425
2018-02-07 16:09:44 -08:00
cd25939be9 build(aio): update examples to CLI to 1.6.5 (#21222)
PR Close #21222
2018-02-07 16:09:26 -08:00
b58c3527e9 test(ivy): add canonical spec for object literals (#22045)
PR Close #22045
2018-02-07 12:10:16 -08:00
efc67ee5ef fix(ivy): make pipe invocation locality neutral (#22030)
PR Close #22030
2018-02-07 12:09:56 -08:00
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
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
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
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
e56de1025a fix(core): ensure initial value of QueryList length (#21980) (#21982)
Set initial value of `length` to `0`.

Fixes regression introduced by e544742156 (diff-a85dbe0991a7577ea24b49374e9ae90b) where the `length` property ceased to have initial value.

Closes #21980

PR Close #21982
2018-02-07 12:08:44 -08:00
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
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
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
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
eb3bfc25be fix(aio): ensure header-links are visible at <600px (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
94d769de71 refactor(aio): simplify .header-link styles (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
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
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
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
8f6047340e docs(animations): fix typo (disbled --> disabled) (#21695)
PR Close #21695
2018-02-07 12:06:45 -08:00
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
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
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
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
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
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
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
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
5b06069fd9 docs: add changelog for 6.0.0-beta.3 2018-02-07 11:25:45 -08:00
d0f3162e84 release: cut the 6.0.0-beta.3 release 2018-02-07 11:23:15 -08:00
81537cb161 docs: add changelog for 5.2.4 2018-02-07 11:20:14 -08:00
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
2707012181 fix(forms): publish missing types (#19941)
PR Close #19941
2018-02-06 08:02:15 -08:00
4d62be69c5 feat(ivy): memoize array literals in render3 (#21973)
PR Close #21973
2018-02-06 08:01:52 -08:00
7e51e52f55 perf(ivy): improve Uglify configuration in hello world integration test (#21985)
PR Close #21985
2018-02-06 08:01:18 -08:00
e81606c97a fix(core): fix proper propagation of subscriptions in EventEmitter (#22016)
Closes #21999

PR Close #22016
2018-02-06 07:56:33 -08:00
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
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
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
1940b18124 docs: update browser support (#21606)
PR Close #21606
2018-02-05 13:02:13 -08:00
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
0d10b9002e refactor(ivy): simplify bind code (#21881)
PR Close #21881
2018-02-05 13:01:37 -08:00
0c9ec37e26 ci: mark PRs with rejection as not green (#21922)
PR Close #21922
2018-02-05 13:01:11 -08:00
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
ae7bc2238d ci: add config for g3 status (#21996)
Ref #21642
PR Close #21996
2018-02-05 12:59:59 -08:00
5df626bbe1 refactor(ivy): misc refactoring (#22001)
PR Close #22001
2018-02-05 12:59:34 -08:00
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
c3fb820473

Closes #21941

PR Close #21992
2018-02-05 12:58:27 -08:00
3a86940ca5 fix(core): should check Zone existance when scheduleMicroTask (#20656)
PR Close #20656
2018-02-02 07:53:55 -08:00
7b120b5f73 docs: consistency fix in describing a custom tag (#21747)
PR Close #21747
2018-02-02 07:53:18 -08:00
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
d77444b88a fix(aio): update Firebase redirects and SW routes (#21763)
Closes #21377

PR Close #21763
2018-02-02 07:52:30 -08:00
240aed29e0 build(aio): test Service Worker "routing" configuration (#21763)
PR Close #21763
2018-02-02 07:52:30 -08:00
bf29936af9 build(aio): test Firebase hosting redirection configuration (#21763)
PR Close #21763
2018-02-02 07:52:30 -08:00
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
447783e575 docs: add docs for IE (#21824)
PR Close #21824
2018-02-02 07:51:46 -08:00
743d8bc845 feat(ivy): add canonical example of a pipe. (#21834)
PR Close #21834
2018-02-02 07:51:23 -08:00
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
d3c2aa5f95 docs: add missing underline (#21892)
PR Close #21892
2018-02-02 07:49:32 -08:00
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
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
65cf1add97 fix(ivy): remove unnecessary parameter of NgOnChangesFeature (#21879)
PR Close #21879
2018-02-01 08:33:36 -08:00
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
f9fa157a09 docs(aio): add missing closing <code-examle> tag (#21771)
PR Close #21771
2018-02-01 08:31:20 -08:00
eb8ddd2983 feat(compiler-cli): reflect static methods added to classes in metadata (#21926)
PR Close #21926
2018-02-01 08:30:58 -08:00
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
1200 changed files with 36675 additions and 11656 deletions

25
.circleci/bazel.rc Normal file
View File

@ -0,0 +1,25 @@
# These options are enabled when running on CI
# We do this by copying this file to /etc/bazel.bazelrc at the start of the build.
# See remote cache documentation in /docs/BAZEL.md
# Don't be spammy in the logs
build --noshow_progress
# Don't run manual tests
test --test_tag_filters=-manual
# Enable experimental CircleCI bazel remote cache proxy
# See remote cache documentation in /docs/BAZEL.md
build --experimental_remote_spawn_cache --remote_rest_cache=http://localhost:7643
# Prevent unstable environment variables from tainting cache keys
build --experimental_strict_action_env
# Workaround https://github.com/bazelbuild/bazel/issues/3645
# Bazel doesn't calculate the memory ceiling correctly when running under Docker.
# Limit Bazel to consuming resources that fit in CircleCI "medium" class which is the default:
# https://circleci.com/docs/2.0/configuration-reference/#resource_class
build --local_resources=3072,2.0,1.0
# Retry in the event of flakes, eg. https://circleci.com/gh/angular/angular/31309
test --flaky_test_attempts=2

View File

@ -15,6 +15,13 @@
var_1: &docker_image angular/ngcontainer:0.1.0
var_2: &cache_key angular-{{ .Branch }}-{{ checksum "yarn.lock" }}-0.1.0
# See remote cache documentation in /docs/BAZEL.md
var_3: &setup-bazel-remote-cache
run:
name: Start up bazel remote cache proxy
command: ~/bazel-remote-proxy -backend circleci://
background: true
# Settings common to each job
anchor_1: &job_defaults
working_directory: ~/ng
@ -34,14 +41,16 @@ jobs:
steps:
- checkout:
<<: *post_checkout
# Check BUILD.bazel formatting before we have a node_modules directory
# Then we don't need any exclude pattern to avoid checking those files
- run: 'buildifier -mode=check $(find . -type f \( -name BUILD.bazel -or -name BUILD \)) ||
(echo "BUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
# Run the skylark linter to check our Bazel rules
- run: 'find . -type f -name "*.bzl" |
xargs java -jar /usr/local/bin/Skylint_deploy.jar ||
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- run: 'yarn buildifier -mode=check ||
(echo -e "\nBUILD files not formatted. Please run ''yarn buildifier''" ; exit 1)'
- run: 'yarn skylint ||
(echo -e "\n.bzl files have lint errors. Please run ''yarn skylint''"; exit 1)'
- restore_cache:
key: *cache_key
@ -54,6 +63,11 @@ jobs:
steps:
- checkout:
<<: *post_checkout
# See remote cache documentation in /docs/BAZEL.md
- run: .circleci/setup_cache.sh
- run: sudo cp .circleci/bazel.rc /etc/bazel.bazelrc
- *setup-bazel-remote-cache
- restore_cache:
key: *cache_key
@ -62,7 +76,16 @@ jobs:
# Use bazel query so that we explicitly ask for all buildable targets to be built as well
# This avoids waiting for a build command to finish before running the first test
# See https://github.com/bazelbuild/bazel/issues/4257
- run: bazel query --output=label '//modules/... union //packages/... union //tools/...' | xargs bazel test --config=ci
- 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

11
.circleci/setup_cache.sh Executable file
View File

@ -0,0 +1,11 @@
#!/bin/sh
# Install bazel remote cache proxy
# This is temporary until the feature is no longer experimental on CircleCI.
# See remote cache documentation in /docs/BAZEL.md
set -u -e
readonly DOWNLOAD_URL="https://5-116431813-gh.circle-artifacts.com/0/pkg/bazel-remote-proxy-$(uname -s)_$(uname -m)"
curl --fail -o ~/bazel-remote-proxy "$DOWNLOAD_URL"
chmod +x ~/bazel-remote-proxy

View File

@ -25,7 +25,7 @@ ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
## Minimal reproduction of the problem with instructions
<!--
For bug reports please provide the *STEPS TO REPRODUCE* and if possible a *MINIMAL DEMO* of the problem via
https://plnkr.co or similar (you can use this template as a starting point: http://plnkr.co/edit/tpl:AvJOMERrnz94ekVua0u5).
https://stackblitz.com or similar (you can use this template as a starting point: https://stackblitz.com/fork/angular-gitter).
-->
## What is the motivation / use case for changing the behavior?

View File

@ -13,6 +13,30 @@ merge:
# text to show when some checks are failing
failureText: "The following checks are failing:"
# the g3 status will be added to your pull requests if they include files that match the patterns
g3Status:
# set to true to disable
disabled: false
# the name of the status
context: "google3"
# text to show when the status is pending
pendingDesc: "Googler: test this change in google3 http://go/angular-g3sync"
# text to show when the status is success
successDesc: "Does not affect google3"
# list of patterns to check for the files changed by the PR
# this list must be manually kept in sync with google3/third_party/javascript/angular2/copy.bara.sky
include:
- "BUILD.bazel"
- "LICENSE"
- "WORKSPACE"
- "modules/**"
- "packages/**"
# list of patterns to ignore for the files changed by the PR
exclude:
- "packages/language-service/**"
- "**/.gitignore"
- "**/.gitkeep"
# comment that will be added to a PR when there is a conflict, leave empty or set to false to disable
mergeConflictComment: "Hi @{{PRAuthor}}! This PR has merge conflicts due to recent upstream merges.
\nPlease help to unblock it by resolving these conflicts. Thanks!"
@ -26,13 +50,15 @@ merge:
noConflict: true
# list of labels that a PR needs to have, checked with a regexp (e.g. "PR target:" will work for the label "PR target: master")
requiredLabels:
- "PR target:"
- "PR target: *"
- "cla: yes"
# list of labels that a PR shouldn't have, checked after the required labels with a regexp
forbiddenLabels:
- "PR target: TBD"
- "PR action: cleanup"
- "PR action: review"
- "PR state: blocked"
- "cla: no"
# list of PR statuses that need to be successful
@ -54,15 +80,23 @@ merge:
# options for the triage plugin
triage:
# number of the milestone to apply when the issue has not been triaged yet
needsTriageMilestone: 83,
# number of the milestone to apply when the issue is triaged
defaultMilestone: 82,
# arrays of labels that determine if an issue is triaged
triagedLabels:
-
- "type: bug"
- "severity"
- "freq"
- "comp:"
- "type: bug/fix"
- "severity*"
- "freq*"
- "comp: *"
-
- "type: feature"
- "comp:"
- "comp: *"
-
- "type: refactor"
- "comp: *"
-
- "type: RFC / Discussion / question"
- "comp: *"

View File

@ -44,6 +44,7 @@ groups:
all:
users: all
required: 1
rejection_value: -999
# In this group, your self-approval does not count
author_approval:
auto: false

View File

@ -56,7 +56,6 @@ env:
- CI_MODE=aio
- CI_MODE=aio_e2e AIO_SHARD=0
- CI_MODE=aio_e2e AIO_SHARD=1
- CI_MODE=bazel
matrix:
fast_finish: true

View File

@ -32,6 +32,7 @@ filegroup(
"reflect-metadata",
"source-map-support",
"minimist",
"tslib",
] for ext in [
"*.js",
"*.json",

View File

@ -1,3 +1,170 @@
<a name="6.0.0-beta.5"></a>
# [6.0.0-beta.5](https://github.com/angular/angular/compare/6.0.0-beta.4...6.0.0-beta.5) (2018-02-22)
### Bug Fixes
* **animations:** report correct totalTime value even during noOp animations ([#22225](https://github.com/angular/angular/issues/22225)) ([e1bf067](https://github.com/angular/angular/commit/e1bf067))
* **common:** correct mapping of Observable methods ([#20518](https://github.com/angular/angular/issues/20518)) ([2639b4b](https://github.com/angular/angular/commit/2639b4b)), 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)) ([8115edc](https://github.com/angular/angular/commit/8115edc))
* **compiler-cli:** add missing entry point to package, update tsickle ([#22295](https://github.com/angular/angular/issues/22295)) ([28ac244](https://github.com/angular/angular/commit/28ac244))
* **core:** properly handle function without prototype in reflector ([#22284](https://github.com/angular/angular/issues/22284)) ([a7ebf5a](https://github.com/angular/angular/commit/a7ebf5a)), closes [#19978](https://github.com/angular/angular/issues/19978)
* **core:** require factory to be provided for shakeable InjectionToken ([#22207](https://github.com/angular/angular/issues/22207)) ([f755db7](https://github.com/angular/angular/commit/f755db7)), closes [#22205](https://github.com/angular/angular/issues/22205)
* **forms:** set state before emitting a value from ngModelChange ([#21514](https://github.com/angular/angular/issues/21514)) ([3e6a86f](https://github.com/angular/angular/commit/3e6a86f)), closes [#21513](https://github.com/angular/angular/issues/21513)
* **core:** set `preserveWhitespaces` to false by default ([#22046](https://github.com/angular/angular/issues/22046)) ([f1a0632](https://github.com/angular/angular/commit/f1a0632)), closes [#22027](https://github.com/angular/angular/issues/22027)
### Features
* **common:** better error message when non-template element used in NgIf ([#22274](https://github.com/angular/angular/issues/22274)) ([67cf11d](https://github.com/angular/angular/commit/67cf11d)), closes [#16410](https://github.com/angular/angular/issues/16410)
* **compiler-cli:** Check unvalidated combination of ngc and TypeScript ([#22293](https://github.com/angular/angular/issues/22293)) ([3ceee99](https://github.com/angular/angular/commit/3ceee99)), closes [#20669](https://github.com/angular/angular/issues/20669)
* **core:** support metadata reflection for native class types ([#22356](https://github.com/angular/angular/issues/22356)) ([5c89d6b](https://github.com/angular/angular/commit/5c89d6b)), closes [#21731](https://github.com/angular/angular/issues/21731)
* **platform-browser:** do not throw error when Hammer.js not loaded ([#22257](https://github.com/angular/angular/issues/22257)) ([991300b](https://github.com/angular/angular/commit/991300b)), closes [#16992](https://github.com/angular/angular/issues/16992)
* **platform-browser:** fix [#19604](https://github.com/angular/angular/issues/19604), can config hammerOptions ([#21979](https://github.com/angular/angular/issues/21979)) ([1d571b2](https://github.com/angular/angular/commit/1d571b2))
### BREAKING CHANGES
* **animations:** When animation is triggered 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.
* **forms:** ngModelChange is now emitted after the value/validity is updated on its control.
Previously, ngModelChange was emitted before its underlying control was updated.
This was fine if you passed through the value directly through the $event keyword, e.g.
```
<input [(ngModel)]="name" (ngModelChange)="onChange($event)">
onChange(value) {
console.log(value); // would log updated value
}
```
However, if you had a handler for the ngModelChange event that checked the value through the control,
you would get the old value rather than the updated value. e.g:
```
<input #modelDir="ngModel" [(ngModel)]="name" (ngModelChange)="onChange(modelDir)">
onChange(ngModel: NgModel) {
console.log(ngModel.value); // would log old value, not updated value
}
```
Now the value and validity will be updated before the ngModelChange event is emitted,
so the same setup will log the updated value.
```
onChange(ngModel: NgModel) {
console.log(ngModel.value); // will log updated value
}
```
We think this order will be less confusing when the control is checked directly.
You will only need to update your app if it has relied on this bug to keep track of the old control value.
If that is the case, you should be able to track the old value directly by saving it on your component.
<a name="5.2.6"></a>
## [5.2.6](https://github.com/angular/angular/compare/5.2.5...5.2.6) (2018-02-22)
### 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="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
* **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
* **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))
<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)
* **bazel:** allow TS to read ambient typings ([#21876](https://github.com/angular/angular/issues/21876)) ([d57fd0b](https://github.com/angular/angular/commit/d57fd0b)), closes [#21872](https://github.com/angular/angular/issues/21872)
* **bazel:** improve error message for missing assets ([#22096](https://github.com/angular/angular/issues/22096)) ([c5ec8d9](https://github.com/angular/angular/commit/c5ec8d9)), closes [#22095](https://github.com/angular/angular/issues/22095)
* **common:** weaken AsyncPipe transform signature ([#22169](https://github.com/angular/angular/issues/22169)) ([c6bdc83](https://github.com/angular/angular/commit/c6bdc83))
* **compiler:** make unary plus operator consistent to JavaScript ([#22154](https://github.com/angular/angular/issues/22154)) ([1b8ea10](https://github.com/angular/angular/commit/1b8ea10)), 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)) ([c4f841f](https://github.com/angular/angular/commit/c4f841f))
* **core:** ensure initial value of QueryList length ([#21980](https://github.com/angular/angular/issues/21980)) ([#21982](https://github.com/angular/angular/issues/21982)) ([47b73fd](https://github.com/angular/angular/commit/47b73fd)), 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)) ([47b71d9](https://github.com/angular/angular/commit/47b71d9))
* **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)) ([56b9591](https://github.com/angular/angular/commit/56b9591))
* **language-service:** correct instructions to install the language service ([#22000](https://github.com/angular/angular/issues/22000)) ([0b23573](https://github.com/angular/angular/commit/0b23573))
* **platform-browser:** support 0/false/null values in transfer_state ([#22179](https://github.com/angular/angular/issues/22179)) ([da6ab91](https://github.com/angular/angular/commit/da6ab91))
<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)
### Bug Fixes
* **common:** don't convert null to a string when flushing a mock request ([#21417](https://github.com/angular/angular/issues/21417)) ([c4fb696](https://github.com/angular/angular/commit/c4fb696)), 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 ([#22007](https://github.com/angular/angular/issues/22007)) ([ce51ea9](https://github.com/angular/angular/commit/ce51ea9))
* **core:** fix proper propagation of subscriptions in EventEmitter ([#22016](https://github.com/angular/angular/issues/22016)) ([c6645e7](https://github.com/angular/angular/commit/c6645e7)), closes [#21999](https://github.com/angular/angular/issues/21999)
* **core:** should check Zone existance when scheduleMicroTask ([#20656](https://github.com/angular/angular/issues/20656)) ([aa9ba7f](https://github.com/angular/angular/commit/aa9ba7f))
<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)

12
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,12 @@
# 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).

View File

@ -51,7 +51,7 @@ and help you to craft the change so that it is successfully accepted into the pr
Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available.
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like:
We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs, we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us a wealth of important information without going back & forth to you with additional questions like:
- version of Angular used
- 3rd-party libraries and their versions
@ -61,7 +61,7 @@ A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm
We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it.
Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced.
Unfortunately, we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that doesn't have enough info to be reproduced.
You can file new issues by filling out our [new issue form](https://github.com/angular/angular/issues/new).
@ -173,12 +173,12 @@ The **header** is mandatory and the **scope** of the header is optional.
Any line of the commit message cannot be longer 100 characters! This allows the message to be easier
to read on GitHub as well as in various git tools.
Footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
The footer should contain a [closing reference to an issue](https://help.github.com/articles/closing-issues-via-commit-messages/) if any.
Samples: (even more [samples](https://github.com/angular/angular/commits/master))
```
docs(changelog): update change log to beta.5
docs(changelog): update changelog to beta.5
```
```
fix(release): need to depend on latest rxjs and zone.js
@ -203,7 +203,7 @@ Must be one of the following:
* **test**: Adding missing tests or correcting existing tests
### Scope
The scope should be the name of the npm package affected (as perceived by person reading changelog generated from commit messages.
The scope should be the name of the npm package affected (as perceived by the person reading the changelog generated from commit messages.
The following is the list of supported scopes:
@ -232,10 +232,10 @@ There are currently a few exceptions to the "use package name" rule:
* none/empty string: useful for `style`, `test` and `refactor` changes that are done across all packages (e.g. `style: add missing semicolons`)
### Subject
The subject contains succinct description of the change:
The subject contains a succinct description of the change:
* use the imperative, present tense: "change" not "changed" nor "changes"
* don't capitalize first letter
* don't capitalize the first letter
* no dot (.) at the end
### Body
@ -266,7 +266,7 @@ changes to be accepted, the CLA must be signed. It's a quick process, we promise
* https://help.github.com/articles/setting-your-commit-email-address-in-git/
* https://stackoverflow.com/questions/37245303/what-does-usera-committed-with-userb-13-days-ago-on-github-mean
* https://help.github.com/articles/about-commit-email-addresses/
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
* https://help.github.com/articles/blocking-command-line-pushes-that-expose-your-personal-email-address/
Note that if you have more than one Git identity, it is important to verify that you are logged in with the same ID with which you signed the CLA, before you commit changes. If not, your PR will fail the CLA check.

View File

@ -1,11 +1,14 @@
workspace(name = "angular")
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
# Using a pre-release snapshot to pick up a commit that makes all nodejs_binary
# programs produce source-mapped stack traces and uglify sourcemaps.
RULES_NODEJS_VERSION = "4303cbef12e5e252ad66cc35cff1123e3a44ee83"
git_repository(
http_archive(
name = "build_bazel_rules_nodejs",
remote = "https://github.com/bazelbuild/rules_nodejs.git",
commit = "230d39a391226f51c03448f91eb61370e2e58c42",
url = "https://github.com/bazelbuild/rules_nodejs/archive/%s.zip" % RULES_NODEJS_VERSION,
strip_prefix = "rules_nodejs-%s" % RULES_NODEJS_VERSION,
sha256 = "fccb9a7122f339d89c9994dc0fea33c737dd76e66281d0da0cb841da5f1edec7",
)
load("@build_bazel_rules_nodejs//:defs.bzl", "check_bazel_version", "node_repositories")
@ -13,10 +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"])
git_repository(
RULES_TYPESCRIPT_VERSION = "d3cc5cd72d89aee0e4c2553ae1b99c707ecbef4e"
http_archive(
name = "build_bazel_rules_typescript",
remote = "https://github.com/bazelbuild/rules_typescript.git",
commit = "eb3244363e1cb265c84e723b347926f28c29aa35"
url = "https://github.com/bazelbuild/rules_typescript/archive/%s.zip" % RULES_TYPESCRIPT_VERSION,
strip_prefix = "rules_typescript-%s" % RULES_TYPESCRIPT_VERSION,
sha256 = "a233fcca41c3e59f639ac71c396edb30e9e9716cf8ed5fb20b51ff8910d5d895",
)
load("@build_bazel_rules_typescript//:defs.bzl", "ts_setup_workspace")
@ -28,13 +34,16 @@ local_repository(
path = "node_modules/rxjs/src",
)
git_repository(
# 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(
name = "com_github_bazelbuild_buildtools",
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",
url = "https://github.com/bazelbuild/buildtools/archive/%s.zip" % BAZEL_BUILDTOOLS_VERSION,
strip_prefix = "buildtools-%s" % BAZEL_BUILDTOOLS_VERSION,
sha256 = "dad19224258ed67cbdbae9b7befb785c3b966e5a33b04b3ce58ddb7824b97d73",
)
http_archive(
@ -56,3 +65,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",
)

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": {

View File

@ -105,8 +105,7 @@ The general setup is as follows:
* Open a terminal, ensure the dependencies are installed; run an initial doc generation; then start the doc-viewer:
```bash
yarn
yarn docs
yarn setup
yarn start
```

View File

@ -1,12 +1,12 @@
// #docregion
import { Component, Input, AfterViewInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
import { Component, Input, OnInit, ViewChild, ComponentFactoryResolver, OnDestroy } from '@angular/core';
import { AdDirective } from './ad.directive';
import { AdItem } from './ad-item';
import { AdComponent } from './ad.component';
@Component({
selector: 'app-add-banner',
selector: 'app-ad-banner',
// #docregion ad-host
template: `
<div class="ad-banner">
@ -17,16 +17,15 @@ import { AdComponent } from './ad.component';
// #enddocregion ad-host
})
// #docregion class
export class AdBannerComponent implements AfterViewInit, OnDestroy {
export class AdBannerComponent implements OnInit, OnDestroy {
@Input() ads: AdItem[];
currentAddIndex: number = -1;
currentAdIndex: number = -1;
@ViewChild(AdDirective) adHost: AdDirective;
subscription: any;
interval: any;
constructor(private componentFactoryResolver: ComponentFactoryResolver) { }
ngAfterViewInit() {
ngOnInit() {
this.loadComponent();
this.getAds();
}
@ -36,8 +35,8 @@ export class AdBannerComponent implements AfterViewInit, OnDestroy {
}
loadComponent() {
this.currentAddIndex = (this.currentAddIndex + 1) % this.ads.length;
let adItem = this.ads[this.currentAddIndex];
this.currentAdIndex = (this.currentAdIndex + 1) % this.ads.length;
let adItem = this.ads[this.currentAdIndex];
let componentFactory = this.componentFactoryResolver.resolveComponentFactory(adItem.component);

View File

@ -8,7 +8,7 @@ import { AdItem } from './ad-item';
selector: 'app-root',
template: `
<div>
<app-add-banner [ads]="ads"></app-add-banner>
<app-ad-banner [ads]="ads"></app-ad-banner>
</div>
`
})

View File

@ -15,6 +15,7 @@ describe('Form Validation Tests', function () {
});
tests('Template-Driven Form');
bobTests();
});
describe('Reactive form', () => {

View File

@ -20,7 +20,7 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
// #enddocregion directive-providers
})
export class ForbiddenValidatorDirective implements Validator {
@Input() forbiddenName: string;
@Input('appForbiddenName') forbiddenName: string;
validate(control: AbstractControl): {[key: string]: any} {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)

View File

@ -12,7 +12,7 @@
<!-- #docregion name-with-error-msg -->
<!-- #docregion name-input -->
<input id="name" name="name" class="form-control"
required minlength="4" forbiddenName="bob"
required minlength="4" appForbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel" >
<!-- #enddocregion name-input -->

View File

@ -0,0 +1,131 @@
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 {
}

View File

@ -0,0 +1,66 @@
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

View File

@ -0,0 +1,32 @@
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

View File

@ -0,0 +1,5 @@
import './geolocation';
import './subscribing';
import './creating';
import './multicasting';

View File

@ -0,0 +1,155 @@
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

View File

@ -0,0 +1,33 @@
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

View File

@ -0,0 +1,26 @@
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) {
// ...
}

View File

@ -0,0 +1,18 @@
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
});

View File

@ -0,0 +1,26 @@
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

View File

@ -0,0 +1,20 @@
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
);
}
}

View File

@ -0,0 +1,25 @@
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

View File

@ -0,0 +1,18 @@
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

View File

@ -0,0 +1,21 @@
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

View File

@ -0,0 +1,27 @@
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

View File

@ -0,0 +1,65 @@
// #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

View File

@ -1,5 +1,5 @@
{
"description": "Testing - app.specs",
"description": "Testing - specs",
"files":[
"src/styles.css",

View File

@ -1,5 +0,0 @@
// #docplaster
// #docregion
describe('1st tests', () => {
it('true is true', () => expect(true).toBe(true));
});

View File

@ -1,9 +1,8 @@
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>;
@ -19,8 +18,8 @@ describe('AboutComponent (highlightDirective)', () => {
});
it('should have skyblue <h2>', () => {
const de = fixture.debugElement.query(By.css('h2'));
const bgColor = de.nativeElement.style.backgroundColor;
const h2: HTMLElement = fixture.nativeElement.querySelector('h2');
const bgColor = h2.style.backgroundColor;
expect(bgColor).toBe('skyblue');
});
// #enddocregion tests

View File

@ -3,7 +3,8 @@ 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 { }

View File

@ -0,0 +1,76 @@
// #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!');
});
});

View File

@ -0,0 +1,11 @@
// #docregion
// Reduced version of the initial AppComponent generated by CLI
import { Component } from '@angular/core';
@Component({
selector: 'app-root',
template: '<h1>Welcome to {{title}}!</h1>'
})
export class AppComponent {
title = 'app';
}

View File

@ -1,7 +1,7 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { AboutComponent } from './about.component';
import { AboutComponent } from './about/about.component';
@NgModule({
imports: [

View File

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

View File

@ -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,11 +17,15 @@ 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.component';
import { AboutComponent } from './about/about.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { TwainService } from './shared/twain.service';
import { TwainService } from './twain/twain.service';
import { HeroService, TestHeroService } from './model/testing/test-hero.service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
@ -31,15 +35,19 @@ let location: SpyLocation;
describe('AppComponent & RouterTestingModule', () => {
beforeEach( async(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ]
imports: [ AppModule, RouterTestingModule ],
providers: [
{ provide: HeroService, useClass: TestHeroService }
]
})
.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);
}));
@ -64,7 +72,7 @@ describe('AppComponent & RouterTestingModule', () => {
}));
// Can't navigate to lazy loaded modules with this technique
xit('should navigate to "Heroes" on click', fakeAsync(() => {
xit('should navigate to "Heroes" on click (not working yet)', fakeAsync(() => {
createComponent();
page.heroesLinkDe.nativeElement.click();
advance();
@ -84,9 +92,9 @@ import { HeroListComponent } from './hero/hero-list.component';
let loader: SpyNgModuleFactoryLoader;
///////// Can't get lazy loaded Heroes to work yet
xdescribe('AppComponent & Lazy Loading', () => {
xdescribe('AppComponent & Lazy Loading (not working yet)', () => {
beforeEach( async(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ AppModule, RouterTestingModule ]
})
@ -95,14 +103,11 @@ xdescribe('AppComponent & Lazy Loading', () => {
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();
@ -110,25 +115,24 @@ xdescribe('AppComponent & Lazy Loading', () => {
expectElementOf(HeroListComponent);
}));
xit('can navigate to "Heroes" w/ browser location URL change', fakeAsync(() => {
it('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 /////////
/** Wait a tick, then detect changes */
/**
* Advance to the routed page
* Wait a tick, then detect changes, and tick again
*/
function advance(): void {
tick();
fixture.detectChanges();
tick(); // wait while navigating
fixture.detectChanges(); // update view
tick(); // wait for async data to arrive
}
function createComponent() {
@ -140,8 +144,8 @@ function createComponent() {
router = injector.get(Router);
router.initialNavigation();
spyOn(injector.get(TwainService), 'getQuote')
.and.returnValue(Promise.resolve('Test Quote')); // fakes it
// fake fast async observable
.and.returnValue(asyncData('Test Quote'));
advance();
page = new Page();
@ -151,7 +155,6 @@ class Page {
aboutLinkDe: DebugElement;
dashboardLinkDe: DebugElement;
heroesLinkDe: DebugElement;
recordedEvents: any[] = [];
// for debugging
comp: AppComponent;
@ -159,17 +162,7 @@ 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];

View File

@ -1,69 +1,67 @@
// #docplaster
import { async, ComponentFixture, TestBed
} from '@angular/core/testing';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DebugElement } from '@angular/core';
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
// #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';
import { AppComponent } from './app.component';
import { RouterLinkDirectiveStub } from '../testing';
// #enddocregion setup-schemas
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
// #docregion component-stubs
@Component({selector: 'app-banner', template: ''})
class BannerStubComponent {}
// #enddocregion setup-stubs-w-imports
@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }
@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent {}
// #enddocregion component-stubs
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
describe('AppComponent & TestModule', () => {
// #docregion setup-stubs, setup-stubs-w-imports
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion testbed-stubs
TestBed.configureTestingModule({
declarations: [
AppComponent,
BannerComponent, WelcomeStubComponent,
RouterLinkStubDirective, RouterOutletStubComponent
RouterLinkDirectiveStub,
BannerStubComponent,
RouterOutletStubComponent,
WelcomeStubComponent
]
})
.compileComponents()
.then(() => {
// #enddocregion testbed-stubs
.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', () => {
// #docregion setup-schemas
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion no-errors-schema, mixed-setup
TestBed.configureTestingModule({
declarations: [ AppComponent, RouterLinkStubDirective ],
schemas: [ NO_ERRORS_SCHEMA ]
declarations: [
AppComponent,
// #enddocregion no-errors-schema
BannerStubComponent,
// #docregion no-errors-schema
RouterLinkDirectiveStub
],
schemas: [ NO_ERRORS_SCHEMA ]
})
.compileComponents()
.then(() => {
// #enddocregion no-errors-schema, mixed-setup
.compileComponents().then(() => {
fixture = TestBed.createComponent(AppComponent);
comp = fixture.componentInstance;
});
}));
// #enddocregion setup-schemas
tests();
});
@ -75,7 +73,7 @@ import { AppRoutingModule } from './app-routing.module';
describe('AppComponent & AppModule', () => {
beforeEach( async(() => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [ AppModule ]
@ -88,7 +86,7 @@ describe('AppComponent & AppModule', () => {
imports: [ AppRoutingModule ]
},
add: {
declarations: [ RouterLinkStubDirective, RouterOutletStubComponent ]
declarations: [ RouterLinkDirectiveStub, RouterOutletStubComponent ]
}
})
@ -104,40 +102,40 @@ describe('AppComponent & AppModule', () => {
});
function tests() {
let links: RouterLinkStubDirective[];
let routerLinks: RouterLinkDirectiveStub[];
let linkDes: DebugElement[];
// #docregion test-setup
beforeEach(() => {
// trigger initial data binding
fixture.detectChanges();
fixture.detectChanges(); // trigger initial data binding
// find DebugElements with an attached RouterLinkStubDirective
linkDes = fixture.debugElement
.queryAll(By.directive(RouterLinkStubDirective));
.queryAll(By.directive(RouterLinkDirectiveStub));
// get the attached link directive instances using the DebugElement injectors
links = linkDes
.map(de => de.injector.get(RouterLinkStubDirective) as RouterLinkStubDirective);
// get attached link directive instances
// using each DebugElement's injector
routerLinks = linkDes.map(de => de.injector.get(RouterLinkDirectiveStub));
});
// #enddocregion test-setup
it('can instantiate it', () => {
it('can instantiate the component', () => {
expect(comp).not.toBeNull();
});
// #docregion tests
it('can get RouterLinks from template', () => {
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');
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');
});
it('can click Heroes link in template', () => {
const heroesLinkDe = linkDes[1];
const heroesLink = links[1];
const heroesLinkDe = linkDes[1]; // heroes link DebugElement
const heroesLink = routerLinks[1]; // heroes link directive
expect(heroesLink.navigatedTo).toBeNull('link should not have navigated yet');
expect(heroesLink.navigatedTo).toBeNull('should not have navigated yet');
heroesLinkDe.triggerEventHandler('click', null);
fixture.detectChanges();

View File

@ -1,29 +1,50 @@
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.component';
import { BannerComponent } from './banner.component';
import { AboutComponent } from './about/about.component';
import { BannerComponent } from './banner/banner.component';
import { HeroService } from './model/hero.service';
import { UserService } from './model/user.service';
import { HeroService } from './model/hero.service';
import { TwainService } from './shared/twain.service';
import { WelcomeComponent } from './welcome.component';
import { TwainComponent } from './twain/twain.component';
import { TwainService } from './twain/twain.service';
import { WelcomeComponent } from './welcome/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
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 }
)
],
providers: [ HeroService, TwainService, UserService ],
declarations: [ AppComponent, AboutComponent, BannerComponent, WelcomeComponent ],
bootstrap: [ AppComponent ]
providers: [
HeroService,
TwainService,
UserService
],
declarations: [
AppComponent,
AboutComponent,
BannerComponent,
TwainComponent,
WelcomeComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@ -1,130 +0,0 @@
// #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

View File

@ -1,681 +0,0 @@
// #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';
}

View File

@ -1,55 +0,0 @@
// #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

View File

@ -1,53 +0,0 @@
// #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');
});
});

View File

@ -0,0 +1,72 @@
// #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');
});
}
});

View File

@ -1,11 +1,14 @@
// #docplaster
// #docregion
import { Component } from '@angular/core';
// #docregion metadata
@Component({
selector: 'app-banner',
template: '<h1>{{title}}</h1>'
templateUrl: './banner-external.component.html',
styleUrls: ['./banner-external.component.css']
})
// #enddocregion metadata
export class BannerComponent {
title = 'Test Tour of Heroes';
}

View File

@ -0,0 +1,119 @@
// #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

View File

@ -0,0 +1,10 @@
// 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 { }

View File

@ -7,53 +7,45 @@ 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 de: DebugElement;
let el: HTMLElement;
let h1: HTMLElement;
beforeEach(async(() => {
beforeEach(() => {
// #docregion auto-detect
TestBed.configureTestingModule({
declarations: [ BannerComponent ],
providers: [
{ provide: ComponentFixtureAutoDetect, useValue: true }
]
})
});
// #enddocregion auto-detect
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(BannerComponent);
comp = fixture.componentInstance;
de = fixture.debugElement.query(By.css('h1'));
el = de.nativeElement;
h1 = fixture.nativeElement.querySelector('h1');
});
// #docregion auto-detect-tests
it('should display original title', () => {
// Hooray! No `fixture.detectChanges()` needed
expect(el.textContent).toContain(comp.title);
expect(h1.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(el.textContent).toContain(oldTitle);
expect(h1.textContent).toContain(oldTitle);
});
it('should display updated title after detectChanges', () => {
comp.title = 'Test Title';
fixture.detectChanges(); // detect changes explicitly
expect(el.textContent).toContain(comp.title);
expect(h1.textContent).toContain(comp.title);
});
// #enddocregion auto-detect-tests
});

View File

@ -0,0 +1,56 @@
// #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
});

View File

@ -1,12 +1,12 @@
// #docregion
import { Component } from '@angular/core';
// #docregion component
@Component({
selector: 'app-banner',
templateUrl: './banner.component.html',
styleUrls: ['./banner.component.css']
template: '<h1>{{title}}</h1>',
styles: ['h1 { color: green; font-size: 350%}']
})
export class BannerComponent {
title = 'Test Tour of Heroes';
}
// #enddocregion component

View File

@ -1,4 +0,0 @@
<!-- #docregion -->
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>

View File

@ -1,7 +1,9 @@
// #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';
@ -11,64 +13,96 @@ 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 heroEl: DebugElement;
let heroDe: DebugElement;
let heroEl: HTMLElement;
// #docregion setup, compile-components
// async beforeEach
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion setup, config-testbed
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent ],
declarations: [ DashboardHeroComponent ]
})
.compileComponents(); // compile template and css
// #enddocregion setup, config-testbed
.compileComponents();
}));
// #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
// pretend that it was wired to something that supplied a hero
expectedHero = new Hero(42, 'Test Name');
// 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
comp.hero = expectedHero;
fixture.detectChanges(); // trigger initial data binding
// trigger initial data binding
fixture.detectChanges();
// #enddocregion setup
});
// #enddocregion setup
// #docregion name-test
it('should display hero name', () => {
it('should display hero name in uppercase', () => {
const expectedPipedName = expectedHero.name.toUpperCase();
expect(heroEl.nativeElement.textContent).toContain(expectedPipedName);
expect(heroEl.textContent).toContain(expectedPipedName);
});
// #enddocregion name-test
// #docregion click-test
it('should raise selected event when clicked', () => {
it('should raise selected event when clicked (triggerEventHandler)', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
// #docregion trigger-event-handler
heroEl.triggerEventHandler('click', null);
heroDe.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', () => {
let selectedHero: Hero;
comp.selected.subscribe((hero: Hero) => selectedHero = hero);
// #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)', () => {
let selectedHero: Hero;
comp.selected.subscribe(hero => selectedHero = hero);
click(heroDe); // click helper with DebugElement
click(heroEl); // click helper with native element
click(heroEl); // triggerEventHandler helper
expect(selectedHero).toBe(expectedHero);
});
// #enddocregion click-test-2
// #enddocregion click-test-3
});
//////////////////
@ -76,28 +110,31 @@ describe('DashboardHeroComponent when tested directly', () => {
describe('DashboardHeroComponent when inside a test host', () => {
let testHost: TestHostComponent;
let fixture: ComponentFixture<TestHostComponent>;
let heroEl: DebugElement;
let heroEl: HTMLElement;
// #docregion test-host-setup
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion test-host-setup
TestBed.configureTestingModule({
declarations: [ DashboardHeroComponent, TestHostComponent ], // declare both
}).compileComponents();
declarations: [ DashboardHeroComponent, TestHostComponent ]
})
// #enddocregion test-host-setup
.compileComponents();
}));
beforeEach(() => {
// #docregion test-host-setup
// create TestHostComponent instead of DashboardHeroComponent
fixture = TestBed.createComponent(TestHostComponent);
testHost = fixture.componentInstance;
heroEl = fixture.debugElement.query(By.css('.hero')); // find hero
heroEl = fixture.nativeElement.querySelector('.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.nativeElement.textContent).toContain(expectedPipedName);
expect(heroEl.textContent).toContain(expectedPipedName);
});
it('should raise selected event when clicked', () => {
@ -114,10 +151,12 @@ 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 = new Hero(42, 'Test Name');
hero: Hero = {id: 42, name: 'Test Name' };
selectedHero: Hero;
onSelected(hero: Hero) { this.selectedHero = hero; }
}

View File

@ -5,13 +5,17 @@ import { Hero } from '../model/hero';
// #docregion component
@Component({
selector: 'dashboard-hero',
templateUrl: './dashboard-hero.component.html',
selector: 'dashboard-hero',
template: `
<div (click)="click()" class="hero">
{{hero.name | uppercase}}
</div>`,
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
// #enddocregion component, class

View File

@ -1,24 +1,24 @@
import { Router } from '@angular/router';
import { DashboardComponent } from './dashboard.component';
import { Hero } from '../model';
import { Hero } from '../model/hero';
import { addMatchers } from '../../testing';
import { FakeHeroService } from '../model/testing';
import { TestHeroService, HeroService } from '../model/testing/test-hero.service';
class FakeRouter {
navigateByUrl(url: string) { return url; }
}
describe('DashboardComponent: w/o Angular TestBed', () => {
describe('DashboardComponent class only', () => {
let comp: DashboardComponent;
let heroService: FakeHeroService;
let heroService: TestHeroService;
let router: Router;
beforeEach(() => {
addMatchers();
router = new FakeRouter() as any as Router;
heroService = new FakeHeroService();
heroService = new TestHeroService();
comp = new DashboardComponent(router, heroService);
});
@ -35,17 +35,19 @@ describe('DashboardComponent: w/o Angular TestBed', () => {
it('should HAVE heroes after HeroService gets them', (done: DoneFn) => {
comp.ngOnInit(); // ngOnInit -> getHeroes
heroService.lastPromise // the one from getHeroes
.then(() => {
heroService.lastResult // the one from getHeroes
.subscribe(
() => {
// throw new Error('deliberate error'); // see it fail gracefully
expect(comp.heroes.length).toBeGreaterThan(0,
'should have heroes after service promise resolves');
})
.then(done, done.fail);
done();
},
done.fail);
});
it('should tell ROUTER to navigate by hero id', () => {
const hero = new Hero(42, 'Abbracadabra');
const hero: Hero = {id: 42, name: 'Abbracadabra' };
const spy = spyOn(router, 'navigateByUrl');
comp.gotoDetail(hero);

View File

@ -2,9 +2,9 @@
import { async, inject, ComponentFixture, TestBed
} from '@angular/core/testing';
import { addMatchers, click } from '../../testing';
import { HeroService } from '../model';
import { FakeHeroService } from '../model/testing';
import { addMatchers, asyncData, click } from '../../testing';
import { HeroService } from '../model/hero.service';
import { getTestHeroes } from '../model/testing/test-heroes';
import { By } from '@angular/platform-browser';
import { Router } from '@angular/router';
@ -12,12 +12,6 @@ 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;
@ -37,8 +31,8 @@ describe('DashboardComponent (deep)', () => {
tests(clickForDeep);
function clickForDeep() {
// get first <div class="hero"> DebugElement
const heroEl = fixture.debugElement.query(By.css('.hero'));
// get first <div class="hero">
const heroEl: HTMLElement = fixture.nativeElement.querySelector('.hero');
click(heroEl);
}
});
@ -61,24 +55,32 @@ describe('DashboardComponent (shallow)', () => {
function clickForShallow() {
// get first <dashboard-hero> DebugElement
const heroEl = fixture.debugElement.query(By.css('dashboard-hero'));
heroEl.triggerEventHandler('selected', comp.heroes[0]);
const heroDe = fixture.debugElement.query(By.css('dashboard-hero'));
heroDe.triggerEventHandler('selected', comp.heroes[0]);
}
});
/** Add TestBed providers, compile, and create DashboardComponent */
function compileAndCreate() {
// #docregion compile-and-create-body
beforeEach( async(() => {
beforeEach(async(() => {
// #docregion router-spy
const routerSpy = jasmine.createSpyObj('Router', ['navigateByUrl']);
const heroServiceSpy = jasmine.createSpyObj('HeroService', ['getHeroes']);
TestBed.configureTestingModule({
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub }
{ provide: HeroService, useValue: heroServiceSpy },
{ provide: Router, useValue: routerSpy }
]
})
// #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
}));
@ -104,8 +106,11 @@ function tests(heroClick: Function) {
describe('after get dashboard heroes', () => {
let router: Router;
// Trigger component so it gets heroes and binds to them
beforeEach( async(() => {
beforeEach(async(() => {
router = fixture.debugElement.injector.get(Router);
fixture.detectChanges(); // runs ngOnInit -> getHeroes
fixture.whenStable() // No need for the `lastPromise` hack!
.then(() => fixture.detectChanges()); // bind to heroes
@ -119,29 +124,25 @@ 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.debugElement.queryAll(By.css('dashboard-hero'));
const heroes = fixture.nativeElement.querySelectorAll('dashboard-hero');
expect(heroes.length).toBe(4, 'should display 4 heroes');
});
// #docregion navigate-test, inject
it('should tell ROUTER to navigate when hero clicked',
inject([Router], (router: Router) => { // ...
// #enddocregion inject
const spy = spyOn(router, 'navigateByUrl');
// #docregion navigate-test
it('should tell ROUTER to navigate when hero clicked', () => {
heroClick(); // trigger click on first inner <div class="hero">
// args passed to router.navigateByUrl()
// args passed to router.navigateByUrl() spy
const spy = router.navigateByUrl as jasmine.Spy;
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');
// #docregion inject
}));
// #enddocregion navigate-test, inject
});
// #enddocregion navigate-test
});
}

View File

@ -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()
.then(heroes => this.heroes = heroes.slice(1, 5));
.subscribe(heroes => this.heroes = heroes.slice(1, 5));
}
// #docregion goto-detail

View File

@ -1,7 +1,8 @@
// tslint:disable-next-line:no-unused-variable
import { async, fakeAsync, tick } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { delay } from 'rxjs/operators';
describe('Angular async helper', () => {
let actuallyDone = false;
@ -34,8 +35,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: any) => {
const source = Observable.of(true).delay(10);
it('should run async test with successful delayed Observable', (done: DoneFn) => {
const source = of(true).pipe(delay(10));
source.subscribe(
val => actuallyDone = true,
err => fail(err),
@ -46,7 +47,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 = Observable.of(true).delay(10);
// const source = of(true).pipe(delay(10));
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)
@ -56,7 +57,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 = Observable.of(true).delay(10);
// const source = of(true).pipe(delay(10));
// source.subscribe(
// val => actuallyDone = true,
// err => fail(err)

View File

@ -1,5 +1,5 @@
// main app entry point
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { BagModule } from './bag';
import { DemoModule } from './demo';
platformBrowserDynamic().bootstrapModule(BagModule);
platformBrowserDynamic().bootstrapModule(DemoModule);

View File

@ -0,0 +1,153 @@
// #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
});

View File

@ -0,0 +1,706 @@
// #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';
}

View File

@ -6,9 +6,8 @@ import { Component, ContentChildren, Directive, EventEmitter,
Pipe, PipeTransform,
SimpleChange } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/delay';
import { of } from 'rxjs/observable/of';
import { delay } from 'rxjs/operators';
////////// The App: Services and Components for the tests. //////////////
@ -17,37 +16,31 @@ export class Hero {
}
////////// Services ///////////////
// #docregion FancyService
// #docregion ValueService
@Injectable()
export class FancyService {
export class ValueService {
protected value = 'real value';
getValue() { return this.value; }
setValue(value: string) { this.value = value; }
getAsyncValue() { return Promise.resolve('async value'); }
getObservableValue() { return of('observable value'); }
getObservableValue() { return Observable.of('observable value'); }
getTimeoutValue() {
return new Promise((resolve) => {
setTimeout(() => { resolve('timeout value'); }, 10);
});
}
getPromiseValue() { return Promise.resolve('promise value'); }
getObservableDelayValue() {
return Observable.of('observable delay value').delay(10);
return of('observable delay value').pipe(delay(10));
}
}
// #enddocregion FancyService
// #enddocregion ValueService
// #docregion DependentService
// #docregion MasterService
@Injectable()
export class DependentService {
constructor(private dependentService: FancyService) { }
getValue() { return this.dependentService.getValue(); }
export class MasterService {
constructor(private masterService: ValueService) { }
getValue() { return this.masterService.getValue(); }
}
// #enddocregion DependentService
// #enddocregion MasterService
/////////// Pipe ////////////////
/*
@ -102,19 +95,19 @@ export class BankAccountParentComponent {
isClosed = true;
}
// #docregion ButtonComp
// #docregion LightswitchComp
@Component({
selector: 'button-comp',
selector: 'lightswitch-comp',
template: `
<button (click)="clicked()">Click me!</button>
<span>{{message}}</span>`
})
export class ButtonComponent {
export class LightswitchComponent {
isOn = false;
clicked() { this.isOn = !this.isOn; }
get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }
}
// #enddocregion ButtonComp
// #enddocregion LightswitchComp
@Component({
selector: 'child-1',
@ -231,31 +224,31 @@ export class MyIfComponent {
@Component({
selector: 'my-service-comp',
template: `injected value: {{fancyService.value}}`,
providers: [FancyService]
template: `injected value: {{valueService.value}}`,
providers: [ValueService]
})
export class TestProvidersComponent {
constructor(public fancyService: FancyService) {}
constructor(public valueService: ValueService) {}
}
@Component({
selector: 'my-service-comp',
template: `injected value: {{fancyService.value}}`,
viewProviders: [FancyService]
template: `injected value: {{valueService.value}}`,
viewProviders: [ValueService]
})
export class TestViewProvidersComponent {
constructor(public fancyService: FancyService) {}
constructor(public valueService: ValueService) {}
}
@Component({
selector: 'external-template-comp',
templateUrl: './bag-external-template.html'
templateUrl: './demo-external-template.html'
})
export class ExternalTemplateComponent implements OnInit {
serviceValue: string;
constructor(@Optional() private service: FancyService) { }
constructor(@Optional() private service: ValueService) { }
ngOnInit() {
if (this.service) { this.serviceValue = this.service.getValue(); }
@ -376,9 +369,9 @@ export class ReversePipeComponent {
export class ShellComponent { }
@Component({
selector: 'bag-comp',
selector: 'demo-comp',
template: `
<h1>Specs Bag</h1>
<h1>Specs Demo</h1>
<my-if-parent-comp></my-if-parent-comp>
<hr>
<h3>Input/Output Component</h3>
@ -397,7 +390,7 @@ export class ShellComponent { }
<input-value-comp></input-value-comp>
<hr>
<h3>Button Component</h3>
<button-comp></button-comp>
<lightswitch-comp></lightswitch-comp>
<hr>
<h3>Needs Content</h3>
<needs-content #nc>
@ -409,13 +402,13 @@ export class ShellComponent { }
</needs-content>
`
})
export class BagComponent { }
export class DemoComponent { }
//////// Aggregations ////////////
export const bagDeclarations = [
BagComponent,
export const demoDeclarations = [
DemoComponent,
BankAccountComponent, BankAccountParentComponent,
ButtonComponent,
LightswitchComponent,
Child1Component, Child2Component, Child3Component,
ExternalTemplateComponent, InnerCompWithExternalTemplateComponent,
InputComponent,
@ -427,7 +420,7 @@ export const bagDeclarations = [
ReversePipe, ReversePipeComponent, ShellComponent
];
export const bagProviders = [DependentService, FancyService];
export const demoProviders = [MasterService, ValueService];
////////////////////
////////////
@ -437,10 +430,10 @@ import { FormsModule } from '@angular/forms';
@NgModule({
imports: [BrowserModule, FormsModule],
declarations: bagDeclarations,
providers: bagProviders,
entryComponents: [BagComponent],
bootstrap: [BagComponent]
declarations: demoDeclarations,
providers: demoProviders,
entryComponents: [DemoComponent],
bootstrap: [DemoComponent]
})
export class BagModule { }
export class DemoModule { }

View File

@ -0,0 +1,15 @@
// 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 {}

View File

@ -1,7 +1,7 @@
import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../model';
import { asyncData, ActivatedRouteStub } from '../../testing';
import { ActivatedRouteStub } from '../../testing';
import { HeroDetailComponent } from './hero-detail.component';
import { Hero } from '../model/hero';
////////// Tests ////////////////////
@ -12,22 +12,21 @@ describe('HeroDetailComponent - no TestBed', () => {
let hds: any;
let router: any;
beforeEach((done: any) => {
expectedHero = new Hero(42, 'Bubba');
activatedRoute = new ActivatedRouteStub();
activatedRoute.testParamMap = { id: expectedHero.id };
beforeEach((done: DoneFn) => {
expectedHero = {id: 42, name: 'Bubba' };
const activatedRoute = new ActivatedRouteStub({ id: expectedHero.id });
router = jasmine.createSpyObj('router', ['navigate']);
hds = jasmine.createSpyObj('HeroDetailService', ['getHero', 'saveHero']);
hds.getHero.and.returnValue(Promise.resolve(expectedHero));
hds.saveHero.and.returnValue(Promise.resolve(expectedHero));
hds.getHero.and.returnValue(asyncData(expectedHero));
hds.saveHero.and.returnValue(asyncData(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.then(done);
hds.getHero.calls.first().returnValue.subscribe(done);
});
it('should expose the hero retrieved from the service', () => {
@ -45,11 +44,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: any) => {
it('should navigate when click save resolves', (done: DoneFn) => {
comp.save();
// waits for async save to complete before navigating
hds.saveHero.calls.first().returnValue
.then(() => {
.subscribe(() => {
expect(router.navigate.calls.any()).toBe(true, 'router.navigate called');
done();
});

View File

@ -3,21 +3,20 @@ import {
async, ComponentFixture, fakeAsync, inject, TestBed, tick
} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { Router } from '@angular/router';
import {
ActivatedRoute, ActivatedRouteStub, click, newEvent, Router, RouterStub
ActivatedRoute, ActivatedRouteStub, asyncData, click, newEvent
} from '../../testing';
import { Hero } from '../model';
import { Hero } from '../model/hero';
import { HeroDetailComponent } from './hero-detail.component';
import { HeroDetailService } from './hero-detail.service';
import { HeroModule } from './hero.module';
////// Testing Vars //////
let activatedRoute: ActivatedRouteStub;
let comp: HeroDetailComponent;
let component: HeroDetailComponent;
let fixture: ComponentFixture<HeroDetailComponent>;
let page: Page;
@ -32,36 +31,38 @@ describe('HeroDetailComponent', () => {
describe('with SharedModule setup', sharedModuleSetup);
});
////////////////////
///////////////////
function overrideSetup() {
// #docregion hds-spy
class HeroDetailServiceSpy {
testHero = new Hero(42, 'Test Hero');
testHero: Hero = {id: 42, name: 'Test Hero' };
/* emit cloned test hero */
getHero = jasmine.createSpy('getHero').and.callFake(
() => Promise
.resolve(true)
.then(() => Object.assign({}, this.testHero))
() => asyncData(Object.assign({}, this.testHero))
);
/* emit clone of test hero, with changes merged in */
saveHero = jasmine.createSpy('saveHero').and.callFake(
(hero: Hero) => Promise
.resolve(true)
.then(() => Object.assign(this.testHero, hero))
(hero: Hero) => asyncData(Object.assign(this.testHero, hero))
);
}
// #enddocregion hds-spy
// the `id` value is irrelevant because ignored by service stub
beforeEach(() => activatedRoute.testParamMap = { id: 99999 } );
beforeEach(() => activatedRoute.setParamMap({ id: 99999 }));
// #docregion setup-override
beforeEach( async(() => {
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
imports: [ HeroModule ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: Router, useClass: RouterStub},
{ provide: Router, useValue: routerSpy},
// #enddocregion setup-override
// HeroDetailService at this level is IRRELEVANT!
{ provide: HeroDetailService, useValue: {} }
@ -87,7 +88,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;
@ -108,7 +109,7 @@ function overrideSetup() {
page.nameInput.value = newName;
page.nameInput.dispatchEvent(newEvent('input')); // tell Angular
expect(comp.hero.name).toBe(newName, 'component hero has new name');
expect(component.hero.name).toBe(newName, 'component hero has new name');
expect(hdsSpy.testHero.name).toBe(origName, 'service hero unchanged before save');
click(page.saveBtn);
@ -116,36 +117,40 @@ function overrideSetup() {
tick(); // wait for async save to complete
expect(hdsSpy.testHero.name).toBe(newName, 'service hero has new name after save');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
}));
// #enddocregion override-tests
it('fixture injected service is not the component injected service',
inject([HeroDetailService], (service: HeroDetailService) => {
// inject gets the service from the fixture
inject([HeroDetailService], (fixtureService: HeroDetailService) => {
expect(service).toEqual(<any> {}, 'service injected from fixture');
expect(hdsSpy).toBeTruthy('service injected into component');
// 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');
}));
}
////////////////////
import { HEROES, FakeHeroService } from '../model/testing';
import { HeroService } from '../model';
import { getTestHeroes, TestHeroService, HeroService } from '../model/testing/test-hero.service';
const firstHero = HEROES[0];
const firstHero = getTestHeroes()[0];
function heroModuleSetup() {
// #docregion setup-hero-module
beforeEach( async(() => {
TestBed.configureTestingModule({
beforeEach(async(() => {
const routerSpy = createRouterSpy();
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: FakeHeroService },
{ provide: Router, useClass: RouterStub},
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
]
})
.compileComponents();
@ -156,9 +161,9 @@ function heroModuleSetup() {
describe('when navigate to existing hero', () => {
let expectedHero: Hero;
beforeEach( async(() => {
beforeEach(async(() => {
expectedHero = firstHero;
activatedRoute.testParamMap = { id: expectedHero.id };
activatedRoute.setParamMap({ id: expectedHero.id });
createComponent();
}));
@ -170,7 +175,7 @@ function heroModuleSetup() {
it('should navigate when click cancel', () => {
click(page.cancelBtn);
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
});
it('should save when click save but not navigate immediately', () => {
@ -181,30 +186,31 @@ function heroModuleSetup() {
click(page.saveBtn);
expect(saveSpy.calls.any()).toBe(true, 'HeroDetailService.save called');
expect(page.navSpy.calls.any()).toBe(false, 'router.navigate not called');
expect(page.navigateSpy.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.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.navigateSpy.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
page.nameInput.value = inputName;
nameInput.value = inputName;
// dispatch a DOM event so that Angular learns of input value change.
page.nameInput.dispatchEvent(newEvent('input'));
nameInput.dispatchEvent(newEvent('input'));
// Tell Angular to update the output span through the title pipe
fixture.detectChanges();
expect(page.nameDisplay.textContent).toBe(titleCaseName);
expect(nameDisplay.textContent).toBe(titleCaseName);
});
// #enddocregion title-case-pipe
// #enddocregion selected-tests
@ -214,10 +220,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(comp.hero.id).toBe(0);
expect(component.hero.id).toBe(0);
});
it('should display empty hero name', () => {
@ -228,14 +234,14 @@ function heroModuleSetup() {
// #docregion route-bad-id
describe('when navigate to non-existent hero id', () => {
beforeEach( async(() => {
activatedRoute.testParamMap = { id: 99999 };
beforeEach(async(() => {
activatedRoute.setParamMap({ id: 99999 });
createComponent();
}));
it('should try to navigate back to hero list', () => {
expect(page.gotoSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navSpy.calls.any()).toBe(true, 'router.navigate called');
expect(page.gotoListSpy.calls.any()).toBe(true, 'comp.gotoList called');
expect(page.navigateSpy.calls.any()).toBe(true, 'router.navigate called');
});
});
// #enddocregion route-bad-id
@ -263,23 +269,25 @@ import { TitleCasePipe } from '../shared/title-case.pipe';
function formsModuleSetup() {
// #docregion setup-forms-module
beforeEach( async(() => {
TestBed.configureTestingModule({
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
imports: [ FormsModule ],
declarations: [ HeroDetailComponent, TitleCasePipe ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
]
})
.compileComponents();
}));
// #enddocregion setup-forms-module
it('should display 1st hero\'s name', fakeAsync(() => {
it('should display 1st hero\'s name', async(() => {
const expectedHero = firstHero;
activatedRoute.testParamMap = { id: expectedHero.id };
activatedRoute.setParamMap({ id: expectedHero.id });
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
@ -291,23 +299,25 @@ import { SharedModule } from '../shared/shared.module';
function sharedModuleSetup() {
// #docregion setup-shared-module
beforeEach( async(() => {
beforeEach(async(() => {
const routerSpy = createRouterSpy();
TestBed.configureTestingModule({
imports: [ SharedModule ],
declarations: [ HeroDetailComponent ],
providers: [
{ provide: ActivatedRoute, useValue: activatedRoute },
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub},
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy},
]
})
.compileComponents();
}));
// #enddocregion setup-shared-module
it('should display 1st hero\'s name', fakeAsync(() => {
it('should display 1st hero\'s name', async(() => {
const expectedHero = firstHero;
activatedRoute.testParamMap = { id: expectedHero.id };
activatedRoute.setParamMap({ id: expectedHero.id });
createComponent().then(() => {
expect(page.nameDisplay.textContent).toBe(expectedHero.name);
});
@ -320,45 +330,51 @@ function sharedModuleSetup() {
/** Create the HeroDetailComponent, initialize it, set test variables */
function createComponent() {
fixture = TestBed.createComponent(HeroDetailComponent);
comp = fixture.componentInstance;
page = new Page();
component = fixture.componentInstance;
page = new Page(fixture);
// 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 {
gotoSpy: jasmine.Spy;
navSpy: jasmine.Spy;
// 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'); }
saveBtn: DebugElement;
cancelBtn: DebugElement;
nameDisplay: HTMLElement;
nameInput: HTMLInputElement;
gotoListSpy: jasmine.Spy;
navigateSpy: jasmine.Spy;
constructor() {
const router = TestBed.get(Router); // get router from root injector
this.gotoSpy = spyOn(comp, 'gotoList').and.callThrough();
this.navSpy = spyOn(router, 'navigate');
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();
}
/** 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;
}
//// query helpers ////
private query<T>(selector: string): T {
return fixture.nativeElement.querySelector(selector);
}
private queryAll<T>(selector: string): T[] {
return fixture.nativeElement.querySelectorAll(selector);
}
}
// #enddocregion page
function createRouterSpy() {
return jasmine.createSpyObj('Router', ['navigate']);
}

View File

@ -2,7 +2,6 @@
// #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';
@ -29,18 +28,18 @@ export class HeroDetailComponent implements OnInit {
// #docregion ng-on-init
ngOnInit(): void {
// get hero when `id` param changes
this.route.paramMap.subscribe(p => this.getHero(p.has('id') && p.get('id')));
this.route.paramMap.subscribe(pmap => this.getHero(pmap.get('id')));
}
// #enddocregion ng-on-init
private getHero(id: string): void {
// when no id or id===0, create new hero
// when no id or id===0, create new blank hero
if (!id) {
this.hero = new Hero();
this.hero = { id: 0, name: '' } as Hero;
return;
}
this.heroDetailService.getHero(id).then(hero => {
this.heroDetailService.getHero(id).subscribe(hero => {
if (hero) {
this.hero = hero;
} else {
@ -50,7 +49,7 @@ export class HeroDetailComponent implements OnInit {
}
save(): void {
this.heroDetailService.saveHero(this.hero).then(() => this.gotoList());
this.heroDetailService.saveHero(this.hero).subscribe(() => this.gotoList());
}
cancel() { this.gotoList(); }

View File

@ -1,5 +1,8 @@
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';
@ -10,13 +13,15 @@ export class HeroDetailService {
// #enddocregion prototype
// Returns a clone which caller may modify safely
getHero(id: number | string): Promise<Hero> {
getHero(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
return this.heroService.getHero(id).then(hero => {
return hero ? Object.assign({}, hero) : null; // clone or null
});
return this.heroService.getHero(id).pipe(
map(hero => {
return hero ? Object.assign({}, hero) : null; // clone or null
})
);
}
saveHero(hero: Hero) {

View File

@ -6,7 +6,7 @@
margin: 0 0 2em 0;
list-style-type: none;
padding: 0;
width: 10em;
width: 15em;
}
.heroes li {
cursor: pointer;

View File

@ -4,15 +4,18 @@ import { async, ComponentFixture, fakeAsync, TestBed, tick
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { addMatchers, newEvent, Router, RouterStub
} from '../../testing';
import { Router } from '@angular/router';
import { HEROES, FakeHeroService } from '../model/testing';
import { addMatchers, newEvent } from '../../testing';
import { getTestHeroes, TestHeroService } from '../model/testing/test-hero.service';
import { HeroModule } from './hero.module';
import { HeroListComponent } from './hero-list.component';
import { HighlightDirective } from '../shared/highlight.directive';
import { HeroService } from '../model';
import { HeroService } from '../model/hero.service';
const HEROES = getTestHeroes();
let comp: HeroListComponent;
let fixture: ComponentFixture<HeroListComponent>;
@ -22,13 +25,15 @@ let page: Page;
describe('HeroListComponent', () => {
beforeEach( async(() => {
beforeEach(async(() => {
addMatchers();
const routerSpy = jasmine.createSpyObj('Router', ['navigate']);
TestBed.configureTestingModule({
imports: [HeroModule],
providers: [
{ provide: HeroService, useClass: FakeHeroService },
{ provide: Router, useClass: RouterStub}
{ provide: HeroService, useClass: TestHeroService },
{ provide: Router, useValue: routerSpy}
]
})
.compileComponents()
@ -125,15 +130,14 @@ class Page {
navSpy: jasmine.Spy;
constructor() {
this.heroRows = fixture.debugElement.queryAll(By.css('li')).map(de => de.nativeElement);
const heroRowNodes = fixture.nativeElement.querySelectorAll('li');
this.heroRows = Array.from(heroRowNodes);
// Find the first element with an attached HighlightDirective
this.highlightDe = fixture.debugElement.query(By.directive(HighlightDirective));
// Get the component's injected router and spy on it
const router = fixture.debugElement.injector.get(Router);
this.navSpy = spyOn(router, 'navigate');
// Get the component's injected router navigation spy
const routerSpy = fixture.debugElement.injector.get(Router);
this.navSpy = routerSpy.navigate as jasmine.Spy;
};
}

View File

@ -1,6 +1,8 @@
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';
@ -10,7 +12,7 @@ import { HeroService } from '../model/hero.service';
styleUrls: [ './hero-list.component.css' ]
})
export class HeroListComponent implements OnInit {
heroes: Promise<Hero[]>;
heroes: Observable<Hero[]>;
selectedHero: Hero;
constructor(

View File

@ -0,0 +1,26 @@
// #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) };
}
}

View File

@ -0,0 +1,215 @@
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
});

View File

@ -1,30 +1,98 @@
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders, HttpErrorResponse } from '@angular/common/http';
import { Hero } from './hero';
import { HEROES } from './test-heroes';
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' })
};
@Injectable()
/** Dummy HeroService. Pretend it makes real http requests */
export class HeroService {
getHeroes() {
return Promise.resolve(HEROES);
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[]>;
}
getHero(id: number | string): Promise<Hero> {
/** GET hero by id. Return `undefined` when id not found */
getHero<Data>(id: number | string): Observable<Hero> {
if (typeof id === 'string') {
id = parseInt(id as string, 10);
}
return this.getHeroes().then(
heroes => heroes.find(hero => hero.id === id)
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'))
);
}
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);
});
/** 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);
}
}

View File

@ -1,20 +0,0 @@
// #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);
});
});

View File

@ -1,4 +1,8 @@
export class Hero {
constructor(public id = 0, public name = '') { }
clone() { return new Hero(this.id, this.name); }
export interface Hero {
id: number;
name: string;
}
// SystemJS bug:
// TS file must export something real in JS, not just interfaces
export const _dummy = undefined;

View File

@ -1,3 +1,6 @@
/**
* Test the HeroService when implemented with the OLD HttpModule
*/
import {
async, inject, TestBed
} from '@angular/core/testing';
@ -12,14 +15,11 @@ import {
} from '@angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/toPromise';
import { of } from 'rxjs/observable/of';
import { catchError, tap } from 'rxjs/operators';
import { Hero } from './hero';
import { HttpHeroService as HeroService } from './http-hero.service';
import { HttpHeroService } from './http-hero.service';
const makeHeroData = () => [
{ id: 1, name: 'Windstorm' },
@ -29,99 +29,100 @@ const makeHeroData = () => [
] as Hero[];
//////// Tests /////////////
describe('Http-HeroService (mockBackend)', () => {
describe('HttpHeroService (using old HttpModule)', () => {
let backend: MockBackend;
let service: HttpHeroService;
beforeEach( async(() => {
beforeEach( () => {
TestBed.configureTestingModule({
imports: [ HttpModule ],
providers: [
HeroService,
HttpHeroService,
{ provide: XHRBackend, useClass: MockBackend }
]
})
.compileComponents();
}));
});
});
it('can instantiate service when inject service',
inject([HeroService], (service: HeroService) => {
expect(service instanceof HeroService).toBe(true);
}));
it('can instantiate service via DI', () => {
service = TestBed.get(HttpHeroService);
expect(service instanceof HttpHeroService).toBe(true);
});
it('can instantiate service with "new"', inject([Http], (http: Http) => {
it('can instantiate service with "new"', () => {
const http = TestBed.get(Http);
expect(http).not.toBeNull('http should be provided');
let service = new HeroService(http);
expect(service instanceof HeroService).toBe(true, 'new service should be ok');
}));
let service = new HttpHeroService(http);
expect(service instanceof HttpHeroService).toBe(true, 'new service should be ok');
});
it('can provide the mockBackend as XHRBackend',
inject([XHRBackend], (backend: MockBackend) => {
expect(backend).not.toBeNull('backend should be provided');
}));
it('can provide the mockBackend as XHRBackend', () => {
const backend = TestBed.get(XHRBackend);
expect(backend).not.toBeNull('backend should be provided');
});
describe('when getHeroes', () => {
let backend: MockBackend;
let service: HeroService;
let fakeHeroes: Hero[];
let response: Response;
let fakeHeroes: Hero[];
let http: Http;
let response: Response;
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);
}));
beforeEach(() => {
it('should have expected fake heroes (then)', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
backend = TestBed.get(XHRBackend);
http = TestBed.get(Http);
service.getHeroes().toPromise()
// .then(() => Promise.reject('deliberate'))
.then(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
});
})));
service = new HttpHeroService(http);
fakeHeroes = makeHeroData();
let options = new ResponseOptions({status: 200, body: {data: fakeHeroes}});
response = new Response(options);
});
it('should have expected fake heroes (Observable.do)', async(inject([], () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
it('should have expected fake heroes (then)', () => {
backend.connections.subscribe((c: MockConnection) => c.mockRespond(response));
service.getHeroes()
.do(heroes => {
expect(heroes.length).toBe(fakeHeroes.length,
'should have expected no. of heroes');
})
.toPromise();
})));
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
);
});
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));
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));
service.getHeroes()
.do(heroes => {
expect(heroes.length).toBe(0, 'should have no heroes');
})
.toPromise();
})));
service.getHeroes().subscribe(
heroes => {
expect(heroes.length).toBe(0, 'should have no heroes');
},
fail
);
});
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));
it('should treat 404 as an Observable error', () => {
let resp = new Response(new ResponseOptions({status: 404}));
backend.connections.subscribe((c: MockConnection) => c.mockRespond(resp));
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();
})));
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
});
});
});
});

View File

@ -1,3 +1,4 @@
// The OLD Http module. See HeroService for use of the current HttpClient
// #docplaster
// #docregion
import { Injectable } from '@angular/core';
@ -5,12 +6,9 @@ import { Http, Response } from '@angular/http';
import { Headers, RequestOptions } from '@angular/http';
import { Hero } from './hero';
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';
import { Observable } from 'rxjs/Observable';
import { ErrorObservable } from 'rxjs/observable/ErrorObservable';
import { catchError, map, tap } from 'rxjs/operators';
@Injectable()
export class HttpHeroService {
@ -19,16 +17,17 @@ export class HttpHeroService {
constructor (private http: Http) {}
getHeroes (): Observable<Hero[]> {
return this.http.get(this._heroesUrl)
.map(this.extractData)
// .do(data => console.log(data)) // eyeball results in the console
.catch(this.handleError);
return this.http.get(this._heroesUrl).pipe(
map(this.extractData),
// tap(data => console.log(data)), // eyeball results in the console
catchError(this.handleError)
);
}
getHero(id: number | string) {
return this.http
.get('app/heroes/?id=${id}')
.map((r: Response) => r.json().data as Hero[]);
return this.http.get('app/heroes/?id=${id}').pipe(
map((r: Response) => r.json().data as Hero[])
);
}
addHero (name: string): Observable<Hero> {
@ -36,9 +35,10 @@ 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)
.map(this.extractData)
.catch(this.handleError);
return this.http.post(this._heroesUrl, body, options).pipe(
map(this.extractData),
catchError(this.handleError)
);
}
updateHero (hero: Hero): Observable<Hero> {
@ -46,9 +46,10 @@ 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)
.map(this.extractData)
.catch(this.handleError);
return this.http.put(this._heroesUrl, body, options).pipe(
map(this.extractData),
catchError(this.handleError)
);
}
private extractData(res: Response) {
@ -63,6 +64,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 Observable.throw(errMsg);
return new ErrorObservable(errMsg);
}
}

View File

@ -1,11 +0,0 @@
// #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')
];

View File

@ -1,41 +0,0 @@
// 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>;
});
}
}

View File

@ -0,0 +1,192 @@
// #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

View File

@ -0,0 +1,61 @@
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`);
})
);
}
}

View File

@ -0,0 +1,13 @@
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' }
];
}

View File

@ -4,12 +4,16 @@ 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, FormsModule,
HighlightDirective, TitleCasePipe, TwainComponent ],
declarations: [ HighlightDirective, TitleCasePipe, TwainComponent ]
imports: [ CommonModule ],
exports: [
CommonModule,
// SharedModule importers won't have to import FormsModule too
FormsModule,
HighlightDirective,
TitleCasePipe
],
declarations: [ HighlightDirective, TitleCasePipe ]
})
export class SharedModule { }

View File

@ -1,92 +0,0 @@
// #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
});

View File

@ -1,116 +0,0 @@
// #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
});

View File

@ -1,27 +0,0 @@
// #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);
}
}

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