Compare commits

..

119 Commits
4.2.5 ... 4.2.x

Author SHA1 Message Date
d033176774 build(packaging): increase node memory for tests (#18731)
PR Close #18731
2017-08-16 21:56:34 -05:00
5dd1744a0b fix(animations): resolve error when using AnimationBuilder with platform-server (#18642)
Use an injected DOCUMENT instead of assuming the global 'document'
exists.

Fixes #18635.

PR Close #18642
2017-08-16 21:54:07 -05:00
e8b98a71db docs(aio): add Dart as a language option (#18080)
(cherry picked from commit f0beb4d750)
2017-07-13 12:42:55 -07:00
5b03219790 docs(aio): Updated resources to UI Components (#18067)
With https:// the site gives console errors, because it loads fonts with http://
(cherry picked from commit bc3b2ac251)
2017-07-13 12:42:50 -07:00
1a09b237ec build(aio): simplify the primary overload's anchor
The primary link to a member should simply be its name, and not require
the parameter list.

(cherry picked from commit ffda3e41e0)
2017-07-13 12:42:44 -07:00
d83f7a1859 build(aio): improve API docs layout and styling
(cherry picked from commit a301dba68f)
2017-07-13 12:42:39 -07:00
9dba26b686 build(aio): fix githubLinks line numbers
The API docs have changed. The line numbers are now found in
`startingLine` and `endingLine` properties rather than the `location`
property, which moved into the `fileInfo` property anyway.

(cherry picked from commit 04f3a4a7a5)
2017-07-13 12:42:35 -07:00
1c8186150b build(aio): truncate API overview parameters at one line
(cherry picked from commit f06ce9adc8)
2017-07-13 12:42:30 -07:00
62edafd49f build(aio): upgrade to dgeni-packages 0.20.0-rc.5
This fixes unwanted extra overloads in interfaces and abstract classes
This provides the overload grouping support and fixes API doc line numbers

(cherry picked from commit 660eec4a23)
2017-07-13 12:42:27 -07:00
c8b4e21a9a build(aio): render grouped overloads
(cherry picked from commit be3352a084)
2017-07-13 12:42:22 -07:00
0558c6f42a build(aio): truncate API overview parameters at one line
(cherry picked from commit 998049ec9b)
2017-07-13 12:42:15 -07:00
9e72bad661 ci: fast-forward to master/HEAD on all circle jobs (#18101)
This problem was introduced by a bad merge.
(cherry picked from commit a7ea0086ee)
2017-07-13 12:41:47 -07:00
37a44f2f15 docs: adjust the release schedule with the 4.3.0-rc.0 release date
(cherry picked from commit edb8375a5f)
2017-07-11 15:53:29 -07:00
afd5d72bf9 docs(aio): Added resource link to amexio-ng-extensions [duplicate of #17785] (#17790) 2017-07-11 15:31:17 -07:00
3f8c8e5d5c docs(aio): change Angular Module to NgModule (#16964) 2017-07-11 15:31:09 -07:00
c755cc3cb6 docs: fix typos and grammar (#17818) 2017-07-11 15:31:02 -07:00
21433d0ffa docs(benchpress): fix android broken links (#17568) 2017-07-11 15:30:51 -07:00
81c20c3b4e docs: update ECMAScript info (#17801) 2017-07-11 15:30:38 -07:00
cd2ed6f07c docs: improve grammar and wording on CONTRIBUTING.md (#17855) 2017-07-11 15:30:31 -07:00
b01eb6cd0c docs: add @angular/animations to list (#17488)
@angular/animations was missing from the SemVer supported API surface list, now added.
2017-07-11 15:30:25 -07:00
3155255dfc fix(router): fix outdated homepage url in NPM package (#17899) 2017-07-11 15:30:17 -07:00
e166da2c71 docs: fix 404 link to SFEIR website (#17913)
Also, I moved SFEIR training description to the "onsite" section since SFEIR doesn't deliver online training.
2017-07-11 15:30:09 -07:00
ef0e4ebbc1 docs(aio): fix host usage in styleguide (#17932) 2017-07-11 15:30:02 -07:00
959d39cda1 test: git ignore e2e test folders (#17984) 2017-07-11 15:29:56 -07:00
ff6c0de659 docs: update required Node/npm versions in DEVELOPER.md
(cherry picked from commit 3d0406c247)
2017-07-11 15:28:13 -07:00
62f3ab5a3f docs: fix CHANGELOG typo (#18026)
(cherry picked from commit db3bcc939e)
2017-07-11 15:28:06 -07:00
2ed16a51af fix(aio): fix tab animations on CodeTabsComponent
In our attempt to remove the material ripple effect from tab labels, we were
killing all `transform`-based animations on other `md-tab-group` elements, such
as animating the content when entering/leaving. (This wasn't an issue on Chrome,
because it didn't respect our `!important` flag.)
This commit fixes it by properly hiding the ripple effect (using a feature
introduced in angular/material2@e4789c7b8) and allowing other animations to
execute normally.

Fixes #17998

(cherry picked from commit 4d45fe6fb5)
2017-07-11 15:27:56 -07:00
27885c1d15 test(aio): add tests for CodeTabsComponent
(cherry picked from commit 076ea2281f)
2017-07-11 15:27:48 -07:00
e5ce728c9e refactor(aio): remove unnecessary comments/imports, improve indentation
(cherry picked from commit aec39c28d8)
2017-07-11 15:27:33 -07:00
bd1ee3abad docs: Unbreak README Formatting (#18043)
- Makes headers consistent
 - Removes extra `)`
 - Makes browserstack line on a separate line
(cherry picked from commit b9525ece77)
2017-07-11 15:27:08 -07:00
a3ad121c04 docs: does please -> does (#18044)
Fixes grammar issue.
(cherry picked from commit 719101338a)
2017-07-11 15:18:48 -07:00
6218299796 docs: various minor formatting changes (#18046)
(cherry picked from commit e131f6bbe8)
2017-07-11 15:18:40 -07:00
ac862798e9 docs: Fix code example in the ChangeDetectorRef docs (#18051)
Minor example fix: mark the 'ref' constructor parameter as a private property
(cherry picked from commit a9757ec674)
2017-07-11 15:18:32 -07:00
911ce36b42 docs: fix style in helper text (#18055)
(cherry picked from commit 9003770f02)
2017-07-11 15:18:04 -07:00
dbfd7179a8 build(aio): add terms from heading to the search index 2017-07-11 15:08:03 +01:00
207e2febb3 build(aio): capture all the headings from a doc in the vFile.headings property 2017-07-11 15:08:02 +01:00
39c9860b9d build(aio): revert to general purpose search algorithm
Now that we have upgraded to the latest lunr search engine, the results
from the standard `search` method are more appropriate.
So we do not need to create our own special queries to get good results.
2017-07-11 15:08:02 +01:00
3f29348526 ci: add aio/content to aio group
This commit also simplifies the rules for the aio content and marketing groups
2017-07-11 11:55:36 +01:00
09261e6c4d docs(aio): fix dangling links 2017-07-11 11:45:11 +01:00
b61c19a86e build(aio): fix matchUpDirectiveDecorators processor 2017-07-11 11:45:10 +01:00
f6ebb40fbc build(aio): fix mergeDecoratorDocs processor 2017-07-11 11:45:10 +01:00
0ce4542990 docs(aio): fix potential invalid jsdoc tag 2017-07-11 11:45:10 +01:00
8072fe120a build(aio): use dedicated overview for decorator API docs 2017-07-11 11:45:09 +01:00
50e79ace12 build(aio): update doc-gen templates 2017-07-11 11:45:09 +01:00
aa39449ce4 build(aio): fix mergeDecoratorDocs processor 2017-07-11 11:45:09 +01:00
d1434e82df build(aio): fix matchUpDirectiveDecorators processor 2017-07-11 11:45:08 +01:00
6a0d5b5825 build(aio): fix test description 2017-07-11 11:45:08 +01:00
5e4c41fcdf build(aio): refactor filterMemberDocs to be more general
Now it also filters function-overloads, hence the name change.
2017-07-11 11:45:08 +01:00
4c8413986e build(aio): upgrade to dgeni-packages v0.20.0-rc.1 2017-07-11 11:45:07 +01:00
c6aacf5b17 build(aio): remove unused script and GitHub token
Since 808bd4af4, we are no longer pre-verifying PRs before uploading the build
artifacts to the preview server, thus we no longer need the
`travis-preverify-pr.sh` script or the `GITHUB_TEAM_MEMBERSHIP_CHECK_KEY`
variable.
2017-07-11 11:43:15 +01:00
bd4ec5c14c revert:build(aio): upgrade to dgeni-packages v0.20.0-rc.1
This reverts commit ab13f1f659.

It was cherry-picked in error from a PR.
2017-07-10 13:42:13 +01:00
406dd45df1 feat(aio): add API endpoint for notifying about PR updates
This commit adds an API endpoint for notifying the preview server about PR
updates (`/pr-updated`). According to the update, the preview server can take
several actions. Currently, it will only check and (if necessary) update the
PR's preview visibility (but more actions could be supported in the future).
The API can be used with an automatic trigger (e.g. a GitHub webhook) to
instantly update a PR's preview visibility when it changes.

Fixes #16526
2017-07-10 12:32:26 +01:00
9948ccf365 feat(aio): implement a way to check and update a PR\'s preview visibility
Previously, `BuildCreator#changePrVisibility()` would throw an error if the PR's
visibility was already up-to-date or if the PR directory did not exist (e.g. was
removed). This method was only used from inside `BuildCreator#create()`, which
had already checked for the existence of the directories.

This commit renames `changePrVisibility()` to `updatePrVisibility()` and makes
it more "forgiving" (i.e. it will only throw if both public and non-public
directories exist). This allows it to be used on events that may or may not have
caused the PR's visibility to change (e.g. a GitHub webhook triggered whenever a
PR's labels change).
2017-07-10 12:32:25 +01:00
9e8d773e4e refactor(aio): unify error messages for invalid requests to upload-server
Previously, there was a distinction between GET requests to invalid URLs and all
other requests. This was mainly because the upload-server only accepts GET
requests, but that is not a hard limitation and may change in the future.

Thus, it makes sense to return a 404 response for requests to invalid URLs
regardless of the method used.
2017-07-10 12:32:25 +01:00
aea11bca87 refactor(aio): use dedicated constants.ts file for e2e-specific constants 2017-07-10 12:32:24 +01:00
08b4e1edc9 refactor(aio): move script to a more relevant directory 2017-07-10 12:32:23 +01:00
274c229463 fix(aio): fix text blurring when hovering over item in resources
Fixes #17907
2017-07-10 12:28:40 +01:00
ab13f1f659 build(aio): upgrade to dgeni-packages v0.20.0-rc.1 2017-07-10 12:25:18 +01:00
b69f0faee6 docs: add changelog for 4.2.6 2017-07-07 21:48:49 -07:00
9c4fcd370a release: cut the 4.2.6 release 2017-07-07 21:46:08 -07:00
8b8b4cb794 refactor(upgrade): move shareable functionality to UpgradeHelper class (#17971)
This functionality can be potentionally re-used by the dynamic version.
2017-07-07 21:11:55 -07:00
53378749ef fix(upgrade): fix transclusion on upgraded components (#17971)
Previously, only simple, single-slot transclusion worked on upgraded components.
This commit fixes/adds support for the following:

- Multi-slot transclusion.
- Using fallback content when no transclusion content is provided.
- Destroy unused scope (when using fallback content).

Fixes #13271
2017-07-07 21:11:46 -07:00
3e61bf76c4 Revert "fix(upgrade): fix transclusion on upgraded components (#17971)"
This reverts commit 30beb52673.
2017-07-07 21:07:18 -07:00
b4fab438ea Revert "refactor(upgrade): move shareable functionality to UpgradeHelper class (#17971)"
This reverts commit 620c2161f6.
2017-07-07 21:07:01 -07:00
426191bc4e build: update the public API golden files 2017-07-07 20:23:48 -07:00
6307581f54 fix(compiler): fix merge error 2017-07-07 19:10:17 -07:00
591d894329 feat(compiler): adds support for quoted object keys in the parser 2017-07-07 19:06:23 -07:00
48772f189f refactor: fix typos (#18000) 2017-07-07 17:23:15 -07:00
70b1ff11d5 ci(aio): only deploy latest commits to staging/production (#17988)
Fixes #17941
2017-07-07 17:23:15 -07:00
f1c9197e07 feat(aio): use new ngo (#17977) 2017-07-07 17:23:14 -07:00
991f8ad687 fix(compiler-cli): fix relative source paths on windows for extracted msg (#17915)
Fixes #16639
2017-07-07 17:23:13 -07:00
efa45b9299 docs(aio): Update NgStyle and NgClass to use quoted keys 2017-07-07 17:23:13 -07:00
45ae14c461 fix(compiler): emits quoted keys only iff they are quoted in the original template
fixes #14292
2017-07-07 17:23:12 -07:00
5ea9b62d58 fix(compiler): fix types 2017-07-07 17:23:11 -07:00
276357780c fix(compiler): remove i18n markup even if no translations (#17999)
Fixes #11042
2017-07-07 17:23:11 -07:00
2108c23b42 revert: "refactor(compiler-cli): remove the dependency on fs in codegen.ts (#17738)"
This reverts commit b116901400.
2017-07-07 17:23:10 -07:00
d48b7d3a8e fix(animations): ensure :animating queries collect previous animation elements properly 2017-07-07 17:23:10 -07:00
00c97417e3 fix(animations): properly detect state transition changes for object literals 2017-07-07 17:23:09 -07:00
16e6c6e7f9 refactor: move DOCUMENT from platform/browser to common 2017-07-07 17:23:08 -07:00
f9c4c3710e ci: disable sauce-connect logging for realz (#17995)
follow up on #17947
2017-07-07 17:23:08 -07:00
eb90ba1321 fix(aio): activate ServiceWorker updates asap (#17699)
Previouly, whenever a new ServiceWorker update was detected the user was
prompted to update (with a notification). This turned out to be more distracting
than helpful. Also, one would get notifications on all open browser tabs/windows
and had to manually reload each one in order for the whole content (including
the app) to be updated.

This commit changes the update strategy as follows:
- Whenever a new update is detected, it is immediately activated (and all
  tabs/windows will be notified).
- Once an update is activated (regardless of whether the activation was
  initiated by the current tab/window or not), a flag will be set to do a
  "full page navigation" the next time the user navigates to a document.

Benefits:
- All tabs/windows are updated asap.
- The updates are applied authomatically, without the user's needing to do
  anything.
- The updates are applied in a way that:
  a. Ensures that the app and content versions are always compatible.
  b. Does not distract the user from their usual workflow.

NOTE:
The "full page navigation" may cause a flash (while the page is loading from
scratch), but this is expected to be minimal, since at that point almost all
necessary resources are cached by and served from the ServiceWorker.

Fixes #17539
2017-07-07 17:23:07 -07:00
9e626415eb fix(aio): home & marketing styles cleanup (#17926) 2017-07-07 17:23:07 -07:00
f632ca4284 refactor(common): replace Object.assign with the spread operator (#17982)
`Object.assign` is not available in all supported browsers and one had to
provide a polyfill. This commit replaces `Object.assign` with the spread
operator (`...`), which TypeScript will transpile to ES5-compatible code.
2017-07-07 17:23:06 -07:00
620c2161f6 refactor(upgrade): move shareable functionality to UpgradeHelper class (#17971)
This functionality can be potentionally re-used by the dynamic version.
2017-07-07 17:21:38 -07:00
30beb52673 fix(upgrade): fix transclusion on upgraded components (#17971)
Previously, only simple, single-slot transclusion worked on upgraded components.
This commit fixes/adds support for the following:

- Multi-slot transclusion.
- Using fallback content when no transclusion content is provided.
- Destroy unused scope (when using fallback content).

Fixes #13271
2017-07-07 17:21:35 -07:00
fe09e10d02 fix(language-service): ignore hover of symbols not in the TypeScript program (#17969)
Fixes: #17965
2017-07-07 17:21:35 -07:00
ee7d134d8e fix(tsc-wrapped): emit exports metadata in flat modules (#17893)
Fixes: #17888
2017-07-07 17:21:34 -07:00
2ab90578f3 fix(language-service): do not crash when hovering over a label definitions (#17974)
Fixes: #17972
2017-07-07 17:21:33 -07:00
8f7cce38f8 fix(router): encode URLs the same way AngularJS did (closer to spec) (#17890)
fixes #16067
2017-07-07 17:21:33 -07:00
a3180937c1 fix(core): fix re-insertions in the iterable differ (#17891)
fixes #17852
2017-07-07 17:21:32 -07:00
3d52675c1b refactor(compiler-cli): remove the dependency on fs in codegen.ts (#17738) 2017-07-07 17:21:32 -07:00
14afb9d314 feat(aio): serve-and-sync command (#17850) 2017-07-07 17:21:31 -07:00
62b973773f ci: allow chuck to approve animations (#17940) 2017-07-07 17:21:30 -07:00
8bdc921502 docs(aio): add instructions for example e2e tests to README (#17819) 2017-07-07 17:21:30 -07:00
cd99c51a40 docs(aio): add DevExtreme to the UI Components section of resources.json (#17939) 2017-07-07 17:21:29 -07:00
3fb93fdb5e build: disable sauce connect logging (#17947)
this change is expected to mitigate the flakes on CI that occur when
we cat the log in the print-logs.sh
2017-07-07 17:21:29 -07:00
cf5752738a fix(animations): properly handle cancelled animation style application 2017-07-07 17:21:28 -07:00
00de9ff531 fix(animations): properly cleanup query artificats when animation construction fails 2017-07-07 17:21:28 -07:00
12a2099265 fix(router): export missing UrlMatcher and UrlMatchResult types
Fixes #15140
2017-07-07 17:21:27 -07:00
cdc0c8ff58 ci: add angular.io content related groups to .pullapprove.yaml 2017-07-07 17:21:26 -07:00
14fbc2e7c0 build(aio): upgrade to angular@4.2.4 and zone.js 2017-07-07 17:21:26 -07:00
ea8fe9316e build: upgrade yarn to 0.24.6 2017-07-07 17:21:25 -07:00
b658afc3de ci: add Chuck to the owners of core (#17892) 2017-07-07 17:21:25 -07:00
706a48e9c1 docs: add animations package to CONTRIBUTING.md (#15413) 2017-07-07 17:21:24 -07:00
Joe
d472aa201a docs(changelog): Correct typos (#16966) 2017-07-07 17:21:23 -07:00
6df57adab3 docs: README - use correctly encoded link for npm badge (#17707)
closes #14990, an old PR that was approved but to old/difficult to merge
2017-07-07 17:21:23 -07:00
54df1e287d docs: shefali edits to Authors Style Guide (#17853) 2017-07-07 17:21:22 -07:00
6d55a807cd perf(core): refactor NgZone, decrease size by 1.2Kb (#17773)
- Remove getters
- Hide private methods for better property renaming

```
497893 May 31 11:26 core.umd.js
718073 May 31 11:26 core.umd.js.map
217108 May 31 11:26 core.umd.min.js
575092 May 31 11:26 core.umd.min.js.map
```

```
495594 May 31 11:28 core.umd.js
716943 May 31 11:28 core.umd.js.map
215826 May 31 11:28 core.umd.min.js
574401 May 31 11:28 core.umd.min.js.map
```
diff: 1,282
2017-07-07 17:21:22 -07:00
670f2e5599 ci(aio): raise polyfill payload limits 2017-07-06 14:48:32 +01:00
408bb89e8f ci(aio): Fix the payload script only check for changes in aio/scripts 2017-07-03 10:41:01 +01:00
ed45830044 docs(aio): update quickstart to latest cli 2017-07-03 08:25:50 +01:00
37b200010b ci(aio): use valid database path for Firebase payload size upload
Firebase does not allow `.` in the path, so when trying to upload payload size
data for branches like `4.2.x`, the following error is thrown:

```
HTTP Error: 400, Invalid path: Invalid token in path
```

This commit fixes it by replacing `.` with `_` in branch names.
2017-07-03 08:20:31 +01:00
571adbe40e docs(aio): fix visual-studio-2015 typo
This fixes #17283
2017-07-03 08:13:10 +01:00
10bfa8a11e ci(aio): avoid printing too large PWA report (and restore print-logs.sh)
There have been some issues lately with Travis jobs failing due to
`print-log.sh`. This is likely due to trying to print the Lighthouse PWA report,
which is too large.
This commit stops printing that report (since it was rarely used and is pretty
easy to acquire when needed) and restores the `print-logs.sh` script (that was
temporarily removed with dfcca66fd).
2017-07-01 14:41:05 +01:00
c460757d2e docs(aio): Add resources contributor info 2017-07-01 14:39:04 +01:00
1a476b81ab ci(aio): use correct URL for preview in PWA score test
In 4268c8289, the preview URLs were changed to not use the whole SHA, but just
the 7 first characters.
2017-06-30 13:03:08 +01:00
27b87e893c docs(aio): fix quickstart-cli bad HTML escape chars 2017-06-30 12:23:38 +01:00
7080fccb38 fix(aio): restore visible sidenav keyboard focus; shift TOC up
closes #17665
Restores keyboard focus that was removed by commit b8b91d3.
Raises the right-TOC by 20px (96px->76px) because was too far down.

To prevent keyboard focus on hidden child nodes,
also collapses inner expanded nodes when parent node is collapsed.
The implicit parent node of top nodes is always expanded.
2017-06-30 11:59:49 +01:00
54f96bfe27 fix(aio): do not include hidden content in window title
The window title is derived based on the current document's `<h1>` heading. Such
headings may contain hidden/non-visible content (e.g. textual name of font
ligatures: `<i class="material-icons">link</i>`) that should not be included in
the title.

This commit fixes this by using `innerText` (instead of `textContent`) to
extract the visible text from the `<h1>` heading. It will still fall back to
`textContent` on browsers that do not support `innerText` (e.g. Firefox 44).

Fixes #17732
2017-06-30 11:55:00 +01:00
294 changed files with 5012 additions and 2465 deletions

View File

@ -1,18 +1,31 @@
defaults: &defaults
# Configuration file for https://circleci.com/gh/angular/angular
# Note: YAML anchors allow an object to be re-used, reducing duplication.
# The ampersand declares an alias for an object, then later the `<<: *name`
# syntax dereferences it.
# See http://blog.daemonl.com/2016/02/yaml.html
# To validate changes, use an online parser, eg.
# http://yaml-online-parser.appspot.com/
# Settings common to each job
anchor_1: &job_defaults
working_directory: ~/ng
docker:
- image: angular/ngcontainer
# After checkout, rebase on top of master.
# Similar to travis behavior, but not quite the same.
# See https://discuss.circleci.com/t/1662
anchor_2: &post_checkout
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
version: 2
jobs:
lint:
<<: *defaults
<<: *job_defaults
steps:
- checkout:
# After checkout, rebase on top of master.
# Similar to travis behavior, but not quite the same.
# See https://discuss.circleci.com/t/1662
post: git pull --ff-only origin "refs/pull/${CI_PULL_REQUEST//*pull\//}/merge"
<<: *post_checkout
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
@ -21,9 +34,10 @@ jobs:
- run: ./node_modules/.bin/gulp lint
build:
<<: *defaults
<<: *job_defaults
steps:
- checkout
- checkout:
<<: *post_checkout
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}

View File

@ -1,14 +1,14 @@
<!--
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
ISSUES MISSING IMPORTANT INFORMATION MIGHT BE CLOSED WITHOUT INVESTIGATION.
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
-->
## I'm submitting a ...
## I'm submitting a...
<!-- Check one of the following options with "x" -->
<pre><code>
[ ] Regression (behavior that used to work and stopped working in a new release)
[ ] Bug report <!-- Please search github for a similar issue or PR before submitting -->
[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report <!-- Please search GitHub for a similar issue or PR before submitting -->
[ ] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question
@ -32,7 +32,7 @@ https://plnkr.co or similar (you can use this template as a starting point: http
<!-- Describe the motivation or the concrete use case. -->
## Please tell us about your environment
## Environment
<pre><code>
Angular version: X.Y.Z
@ -49,8 +49,8 @@ Browser:
- [ ] Edge version XX
For Tooling issues:
- Node version: XX <!-- use `node --version` -->
- Platform: <!-- Mac, Linux, Windows -->
- Node version: XX <!-- run `node --version` -->
- Platform: <!-- Mac, Linux, Windows -->
Others:
<!-- Anything else relevant? Operating system version, IDE, package manager, HTTP server, ... -->

View File

@ -1,5 +1,5 @@
## PR Checklist
Does please check if your PR fulfills the following requirements:
Please check if your PR fulfills the following requirements:
- [ ] The commit message follows our guidelines: https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
- [ ] Tests for the changes have been added (for bug fixes / features)

1
.gitignore vendored
View File

@ -2,6 +2,7 @@
/dist/
bazel-*
e2e_test.*
node_modules
bower_components

View File

@ -8,9 +8,11 @@
# alexeagle - Alex Eagle
# alxhub - Alex Rickabaugh
# chuckjaz - Chuck Jazdzewski
# Foxandxss - Jesús Rodríguez
# gkalpak - George Kalpakas
# IgorMinar - Igor Minar
# jasonaden - Jason Aden
# juleskremer - Jules Kremer
# kara - Kara Erickson
# matsko - Matias Niemelä
# mhevery - Misko Hevery
@ -18,10 +20,11 @@
# pkozlowski-opensource - Pawel Kozlowski
# robwormald - Rob Wormald
# tbosch - Tobias Bosch
# tinayuangao - Tina Gao
# vicb - Victor Berchet
# vikerman - Vikram Subramanian
# wardbell - Ward Bell
# tinayuangao - Tina Gao
version: 2
@ -93,6 +96,7 @@ groups:
- "packages/core/*"
users:
- tbosch #primary
- chuckjaz
- mhevery
- vicb
- IgorMinar #fallback
@ -104,6 +108,7 @@ groups:
- "packages/platform-browser/animations/*"
users:
- matsko #primary
- chuckjaz #fallback
- mhevery #fallback
- IgorMinar #fallback
@ -249,10 +254,40 @@ groups:
angular.io:
conditions:
files:
- "aio/*"
include:
- "aio/*"
users:
- IgorMinar #primary
- petebacondarwin #secondary
- petebacondarwin #primary
- IgorMinar
- gkalpak
- wardbell
- mhevery #fallback
angular.io-guide-and-tutorial:
conditions:
files:
include:
- "aio/content/*"
exclude:
- "aio/content/marketing/*"
- "aio/content/navigation.json"
- "aio/content/license.md"
users:
- juleskremer #primary
- Foxandxss
- stephenfluin
- wardbell
- IgorMinar #fallback
- mhevery #fallback
angular.io-marketing:
conditions:
files:
include:
- "aio/content/marketing/*"
- "aio/content/navigation.json"
- "aio/content/license.md"
users:
- juleskremer #primary
- stephenfluin
- IgorMinar #fallback
- mhevery #fallback

View File

@ -76,5 +76,4 @@ script:
- ./scripts/ci/angular.sh
# all the scripts under this line will not quickly abort in case ${TRAVIS_TEST_RESULT} is 1 (job failure)
- ./scripts/ci/cleanup.sh
# Disabled so we can debug Travis build failures on Master seeming to coming from printing logs
# - ./scripts/ci/print-logs.sh
- ./scripts/ci/print-logs.sh

View File

@ -1,3 +1,34 @@
<a name="4.2.6"></a>
## [4.2.6](https://github.com/angular/angular/compare/4.2.5...4.2.6) (2017-07-08)
### Bug Fixes
* **animations:** ensure `:animating` queries collect previous animation elements properly ([d48b7d3](https://github.com/angular/angular/commit/d48b7d3))
* **animations:** properly cleanup query artificats when animation construction fails ([00de9ff](https://github.com/angular/angular/commit/00de9ff))
* **animations:** properly detect state transition changes for object literals ([00c9741](https://github.com/angular/angular/commit/00c9741))
* **animations:** properly handle cancelled animation style application ([cf57527](https://github.com/angular/angular/commit/cf57527))
* **compiler:** emits quoted keys only if they are quoted in the original template ([45ae14c](https://github.com/angular/angular/commit/45ae14c)), closes [#14292](https://github.com/angular/angular/issues/14292)
* **compiler:** fix merge error ([6307581](https://github.com/angular/angular/commit/6307581))
* **compiler:** fix types ([5ea9b62](https://github.com/angular/angular/commit/5ea9b62))
* **compiler:** remove i18n markup even if no translations ([#17999](https://github.com/angular/angular/issues/17999)) ([2763577](https://github.com/angular/angular/commit/2763577)), closes [#11042](https://github.com/angular/angular/issues/11042)
* **compiler-cli:** fix relative source paths on windows for extracted msg ([#17915](https://github.com/angular/angular/issues/17915)) ([991f8ad](https://github.com/angular/angular/commit/991f8ad)), closes [#16639](https://github.com/angular/angular/issues/16639)
* **core:** fix re-insertions in the iterable differ ([#17891](https://github.com/angular/angular/issues/17891)) ([a318093](https://github.com/angular/angular/commit/a318093)), closes [#17852](https://github.com/angular/angular/issues/17852)
* **language-service:** do not crash when hovering over a label definitions ([#17974](https://github.com/angular/angular/issues/17974)) ([2ab9057](https://github.com/angular/angular/commit/2ab9057))
* **language-service:** ignore hover of symbols not in the TypeScript program ([#17969](https://github.com/angular/angular/issues/17969)) ([fe09e10](https://github.com/angular/angular/commit/fe09e10))
* **router:** encode URLs the same way AngularJS did (closer to spec) ([#17890](https://github.com/angular/angular/issues/17890)) ([8f7cce3](https://github.com/angular/angular/commit/8f7cce3)), closes [#16067](https://github.com/angular/angular/issues/16067)
* **router:** export missing UrlMatcher and UrlMatchResult types ([12a2099](https://github.com/angular/angular/commit/12a2099)), closes [#15140](https://github.com/angular/angular/issues/15140)
* **tsc-wrapped:** emit exports metadata in flat modules ([#17893](https://github.com/angular/angular/issues/17893)) ([ee7d134](https://github.com/angular/angular/commit/ee7d134))
* **upgrade:** fix transclusion on upgraded components ([#17971](https://github.com/angular/angular/issues/17971)) ([5337874](https://github.com/angular/angular/commit/5337874)), closes [#13271](https://github.com/angular/angular/issues/13271)
* **upgrade:** fix transclusion on upgraded components ([#17971](https://github.com/angular/angular/issues/17971)) ([30beb52](https://github.com/angular/angular/commit/30beb52)), closes [#13271](https://github.com/angular/angular/issues/13271)
### Performance Improvements
* **core:** refactor NgZone, decrease size by 1.2Kb ([#17773](https://github.com/angular/angular/issues/17773)) ([6d55a80](https://github.com/angular/angular/commit/6d55a80))
<a name="4.2.5"></a>
## [4.2.5](https://github.com/angular/angular/compare/4.2.4...4.2.5) (2017-06-30)
@ -168,7 +199,7 @@
### Bug Fixes
* **animations:** make sure reuseable animation subtitutions work without default params ([#16875](https://github.com/angular/angular/issues/16875)) ([7d9f96a](https://github.com/angular/angular/commit/7d9f96a))
* **animations:** make sure reusable animation substitutions work without default params ([#16875](https://github.com/angular/angular/issues/16875)) ([7d9f96a](https://github.com/angular/angular/commit/7d9f96a))
* **animations:** only require one flushMicrotasks call when testing animations ([6cb93c1](https://github.com/angular/angular/commit/6cb93c1))
* **compiler:** avoid a `...null` spread in extraction ([#16547](https://github.com/angular/angular/issues/16547)) ([e0a8376](https://github.com/angular/angular/commit/e0a8376))
* **compiler-cli:** allow '==' to compare nullable types ([#16731](https://github.com/angular/angular/issues/16731)) ([d761059](https://github.com/angular/angular/commit/d761059))
@ -183,7 +214,7 @@
### Features
* **animations:** introduce a wave of new animation features ([16c8167](https://github.com/angular/angular/commit/16c8167))
* **animations:** introduce routeable animation support ([f1a9e3c](https://github.com/angular/angular/commit/f1a9e3c))
* **animations:** introduce routable animation support ([f1a9e3c](https://github.com/angular/angular/commit/f1a9e3c))
* add .ngsummary.ts files to support AOT unit tests ([547c363](https://github.com/angular/angular/commit/547c363))
* introduce `TestBed.overrideProvider` ([#16725](https://github.com/angular/angular/issues/16725)) ([39b92f7](https://github.com/angular/angular/commit/39b92f7))
* **compiler:** support a non-null postfix assert ([#16672](https://github.com/angular/angular/issues/16672)) ([b9521b5](https://github.com/angular/angular/commit/b9521b5))

View File

@ -17,7 +17,7 @@ Help us keep Angular open and inclusive. Please read and follow our [Code of Con
## <a name="question"></a> Got a Question or Problem?
Please, do not open issues for the general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. You've got much better chances of getting your question answered on [Stack Overflow](https://stackoverflow.com/questions/tagged/angular) where the questions should be tagged with tag `angular`.
Stack Overflow is a much better place to ask questions since:
@ -25,7 +25,7 @@ Stack Overflow is a much better place to ask questions since:
- questions and answers stay available for public viewing so your question / answer might help someone else
- Stack Overflow's voting system assures that the best answers are prominently visible.
To save your and our time we will be systematically closing all the issues that are requests for general support and redirecting people to Stack Overflow.
To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Stack Overflow.
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
@ -206,6 +206,7 @@ The scope should be the name of the npm package affected (as perceived by person
The following is the list of supported scopes:
* **animations**
* **common**
* **compiler**
* **compiler-cli**

View File

@ -3,22 +3,21 @@
[![Join the chat at https://gitter.im/angular/angular](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/angular/angular?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/pr?style=flat)](http://issuestats.com/github/angular/angular)
[![Issue Stats](http://issuestats.com/github/angular/angular/badge/issue?style=flat)](http://issuestats.com/github/angular/angular)
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://badge.fury.io/js/%40angular%2Fcore)
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://www.npmjs.com/@angular/core)
[![Sauce Test Status](https://saucelabs.com/browser-matrix/angular2-ci.svg)](https://saucelabs.com/u/angular2-ci)
*Safari (7+), iOS (7+), Edge (14) and IE mobile (11) are tested on [BrowserStack][browserstack].*
Angular
=========
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript (JS) and other languages.
# Angular
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript and other languages.
## Quickstart
[Get started in 5 minutes][quickstart].
## Want to help?
Want to file a bug, contribute some code, or improve documentation? Excellent! Read up on our

View File

@ -16,6 +16,7 @@ Here are the most important tasks you might need to use:
* `yarn setup` - Install all the dependencies, boilerplate, plunkers, zips and runs dgeni on the docs.
* `yarn start` - run a development web server that watches the files; then builds the doc-viewer and reloads the page, as necessary.
* `yarn serve-and-sync` - run both the `docs-watch` and `start` in the same console.
* `yarn lint` - check that the doc-viewer code follows our style rules.
* `yarn test` - watch all the source files, for the doc-viewer, and run all the unit tests when any change.
* `yarn e2e` - run all the e2e tests for the doc-viewer.
@ -30,6 +31,10 @@ Here are the most important tasks you might need to use:
* `yarn generate-plunkers` - generate the plunker files that are used by the `live-example` tags in the docs.
* `yarn generate-zips` - generate the zip files from the examples. Zip available via the `live-example` tags in the docs.
* `yarn example-e2e` - run all e2e tests for examples
- `yarn example-e2e -- --setup` - force webdriver update & other setup, then run tests
- `yarn example-e2e -- --filter=foo` - limit e2e tests to those containing the word "foo"
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
## Using ServiceWorker locally

View File

@ -88,6 +88,21 @@ server {
resolver 127.0.0.1;
}
# Notify about PR changes
location "~^/pr-updated/?$" {
if ($request_method != "POST") {
add_header Allow "POST";
return 405;
}
proxy_pass_request_headers on;
proxy_redirect off;
proxy_method POST;
proxy_pass http://{{$AIO_UPLOAD_HOSTNAME}}:{{$AIO_UPLOAD_PORT}}$request_uri;
resolver 127.0.0.1;
}
# Everything else
location / {
return 404;

View File

@ -18,45 +18,17 @@ export class BuildCreator extends EventEmitter {
}
// Methods - Public
public changePrVisibility(pr: string, makePublic: boolean): Promise<void> {
const {oldPrDir, newPrDir} = this.getCandidatePrDirs(pr, makePublic);
return Promise.
all([this.exists(oldPrDir), this.exists(newPrDir)]).
then(([oldPrDirExisted, newPrDirExisted]) => {
if (!oldPrDirExisted) {
throw new UploadError(404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
} else if (newPrDirExisted) {
throw new UploadError(409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
}
return Promise.resolve().
then(() => shell.mv(oldPrDir, newPrDir)).
then(() => this.listShasByDate(newPrDir)).
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
then(() => undefined);
}).
catch(err => {
if (!(err instanceof UploadError)) {
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
}
throw err;
});
}
public create(pr: string, sha: string, archivePath: string, isPublic: boolean): Promise<void> {
// Use only part of the SHA for more readable URLs.
sha = sha.substr(0, SHORT_SHA_LEN);
const {oldPrDir: otherVisPrDir, newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
const {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
const shaDir = path.join(prDir, sha);
let dirToRemoveOnError: string;
return Promise.resolve().
then(() => this.exists(otherVisPrDir)).
// If the same PR exists with different visibility, update the visibility first.
then(otherVisPrDirExisted => (otherVisPrDirExisted && this.changePrVisibility(pr, isPublic)) as any).
then(() => this.updatePrVisibility(pr, isPublic)).
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
then(([prDirExisted, shaDirExisted]) => {
if (shaDirExisted) {
@ -84,6 +56,36 @@ export class BuildCreator extends EventEmitter {
});
}
public updatePrVisibility(pr: string, makePublic: boolean): Promise<boolean> {
const {oldPrDir: otherVisPrDir, newPrDir: targetVisPrDir} = this.getCandidatePrDirs(pr, makePublic);
return Promise.
all([this.exists(otherVisPrDir), this.exists(targetVisPrDir)]).
then(([otherVisPrDirExisted, targetVisPrDirExisted]) => {
if (!otherVisPrDirExisted) {
// No visibility change: Either the visibility is up-to-date or the PR does not exist.
return false;
} else if (targetVisPrDirExisted) {
// Error: Directories for both visibilities exist.
throw new UploadError(409, `Request to move '${otherVisPrDir}' to existing directory '${targetVisPrDir}'.`);
}
// Visibility change: Moving `otherVisPrDir` to `targetVisPrDir`.
return Promise.resolve().
then(() => shell.mv(otherVisPrDir, targetVisPrDir)).
then(() => this.listShasByDate(targetVisPrDir)).
then(shas => this.emit(ChangedPrVisibilityEvent.type, new ChangedPrVisibilityEvent(+pr, shas, makePublic))).
then(() => true);
}).
catch(err => {
if (!(err instanceof UploadError)) {
err = new UploadError(500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\n${err}`);
}
throw err;
});
}
// Methods - Protected
protected exists(fileOrDir: string): Promise<boolean> {
return new Promise(resolve => fs.access(fileOrDir, err => resolve(!err)));

View File

@ -1,4 +1,5 @@
// Imports
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as http from 'http';
import {GithubPullRequests} from '../common/github-pull-requests';
@ -84,6 +85,7 @@ class UploadServerFactory {
protected createMiddleware(buildVerifier: BuildVerifier, buildCreator: BuildCreator): express.Express {
const middleware = express();
const jsonParser = bodyParser.json();
middleware.get(/^\/create-build\/([1-9][0-9]*)\/([0-9a-f]{40})\/?$/, (req, res) => {
const pr = req.params[0];
@ -96,8 +98,8 @@ class UploadServerFactory {
} else if (!archive) {
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
} else {
buildVerifier.
verify(+pr, authHeader).
Promise.resolve().
then(() => buildVerifier.verify(+pr, authHeader)).
then(verStatus => verStatus === BUILD_VERIFICATION_STATUS.verifiedAndTrusted).
then(isPublic => buildCreator.create(pr, sha, archive, isPublic).
then(() => res.sendStatus(isPublic ? 201 : 202))).
@ -105,8 +107,23 @@ class UploadServerFactory {
}
});
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
middleware.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req));
middleware.post(/^\/pr-updated\/?$/, jsonParser, (req, res) => {
const {action, number: prNo}: {action?: string, number?: number} = req.body;
const visMayHaveChanged = !action || (action === 'labeled') || (action === 'unlabeled');
if (!visMayHaveChanged) {
res.sendStatus(200);
} else if (!prNo) {
this.throwRequestError(400, `Missing or empty 'number' field`, req);
} else {
Promise.resolve().
then(() => buildVerifier.getPrIsTrusted(prNo)).
then(isPublic => buildCreator.updatePrVisibility(String(prNo), isPublic)).
then(() => res.sendStatus(200)).
catch(err => this.respondWithError(res, err));
}
});
middleware.all('*', req => this.throwRequestError(404, 'Unknown resource', req));
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
return middleware;
@ -125,7 +142,10 @@ class UploadServerFactory {
}
protected throwRequestError(status: number, error: string, req: express.Request) {
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
throw new UploadError(status, message);
}
}

View File

@ -0,0 +1,16 @@
// Using the values below, we can fake the response of the corresponding methods in tests. This is
// necessary, because the test upload-server will be running as a separate node process, so we will
// not have direct access to the code (e.g. for mocking).
// (See also 'lib/verify-setup/start-test-upload-server.ts'.)
/* tslint:disable: variable-name */
// Special values to be used as `authHeader` in `BuildVerifier#verify()`.
export const BV_verify_error = 'FAKE_VERIFICATION_ERROR';
export const BV_verify_verifiedNotTrusted = 'FAKE_VERIFIED_NOT_TRUSTED';
// Special values to be used as `pr` in `BuildVerifier#getPrIsTrusted()`.
export const BV_getPrIsTrusted_error = 32203;
export const BV_getPrIsTrusted_notTrusted = 72457;
/* tslint:enable: variable-name */

View File

@ -317,6 +317,51 @@ describe(`nginx`, () => {
});
describe(`${host}/pr-updated`, () => {
const url = `${scheme}://${host}/pr-updated`;
it('should disallow non-POST requests', done => {
Promise.all([
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse([405, 'Not Allowed'])),
]).then(done);
});
it('should pass requests through to the upload server', done => {
const cmdPrefix = `curl -iLX POST --header "Content-Type: application/json"`;
const cmd1 = `${cmdPrefix} ${url}`;
const cmd2 = `${cmdPrefix} --data '{"number":${pr}}' ${url}`;
const cmd3 = `${cmdPrefix} --data '{"number":${pr},"action":"foo"}' ${url}`;
Promise.all([
h.runCmd(cmd1).then(h.verifyResponse(400, /Missing or empty 'number' field/)),
h.runCmd(cmd2).then(h.verifyResponse(200)),
h.runCmd(cmd3).then(h.verifyResponse(200)),
]).then(done);
});
it('should respond with 404 for unknown paths', done => {
const cmdPrefix = `curl -iLX POST ${scheme}://${host}`;
Promise.all([
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
]).then(done);
});
});
describe(`${host}/*`, () => {
it('should respond with 404 for unknown URLs (even if the resource exists)', done => {

View File

@ -1,5 +1,6 @@
// Imports
import * as path from 'path';
import * as c from './constants';
import {helper as h} from './helper';
// Tests
@ -14,12 +15,14 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
const getFile = (pr: string, sha: string, file: string) =>
h.runCmd(`curl -iL ${scheme}://pr${pr}-${h.getShordSha(sha)}.${host}/${file}`);
const uploadBuild = (pr: string, sha: string, archive: string, authHeader = 'Token FOO') => {
// Using `FAKE_VERIFICATION_ERROR` or `FAKE_VERIFIED_NOT_TRUSTED` as `authHeader`,
// we can fake the response of the overwritten `BuildVerifier.verify()` method.
// (See 'lib/upload-server/index-test.ts'.)
const curlPost = `curl -iLX POST --header "Authorization: ${authHeader}"`;
return h.runCmd(`${curlPost} --data-binary "@${archive}" ${scheme}://${host}/create-build/${pr}/${sha}`);
};
const prUpdated = (pr: number, action?: string) => {
const url = `${scheme}://${host}/pr-updated`;
const payloadStr = JSON.stringify({number: pr, action});
return h.runCmd(`curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`);
};
beforeEach(() => jasmine.DEFAULT_TIMEOUT_INTERVAL = 10000);
afterEach(() => {
@ -29,7 +32,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
});
describe('for a new PR', () => {
describe('for a new/non-existing PR', () => {
it('should be able to upload and serve a public build', done => {
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
@ -54,7 +57,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
@ -74,7 +77,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
then(h.verifyResponse(403, errorRegex9)).
then(() => {
expect(h.buildExists(pr9)).toBe(false);
@ -83,6 +86,18 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
then(done);
});
it('should be able to notify that a PR has been updated (and do nothing)', done => {
prUpdated(+pr9).
then(h.verifyResponse(200)).
then(() => {
// The PR should still not exist.
expect(h.buildExists(pr9, '', false)).toBe(false);
expect(h.buildExists(pr9, '', true)).toBe(false);
}).
then(done);
});
});
@ -123,7 +138,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyBuild(pr9, sha0, false);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
then(() => Promise.all([
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
@ -148,7 +163,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyBuild(pr9, sha0);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
then(h.verifyResponse(403, errorRegex9)).
then(() => {
expect(h.buildExists(pr9)).toBe(true);
@ -186,7 +201,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyBuild(pr9, sha9, false);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
then(h.verifyResponse(409)).
then(() => {
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
@ -195,6 +210,110 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
then(done);
});
it('should be able to request re-checking visibility (if outdated)', done => {
const publicPr = pr9;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
h.createDummyBuild(publicPr, sha9, false);
h.createDummyBuild(hiddenPr, sha9, true);
// PR visibilities are outdated (i.e. the opposte of what the should).
expect(h.buildExists(publicPr, '', false)).toBe(true);
expect(h.buildExists(publicPr, '', true)).toBe(false);
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
Promise.
all([
prUpdated(+publicPr).then(h.verifyResponse(200)),
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
]).
then(() => {
// PR visibilities should have been updated.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
}).
then(() => {
h.deletePrDir(publicPr, true);
h.deletePrDir(hiddenPr, false);
}).
then(done);
});
it('should be able to request re-checking visibility (if up-to-date)', done => {
const publicPr = pr9;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
h.createDummyBuild(publicPr, sha9, true);
h.createDummyBuild(hiddenPr, sha9, false);
// PR visibilities are already up-to-date.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
Promise.
all([
prUpdated(+publicPr).then(h.verifyResponse(200)),
prUpdated(+hiddenPr).then(h.verifyResponse(200)),
]).
then(() => {
// PR visibilities are still up-to-date.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
}).
then(done);
});
it('should reject a request if re-checking visibility fails', done => {
const errorPr = String(c.BV_getPrIsTrusted_error);
h.createDummyBuild(errorPr, sha9, true);
expect(h.buildExists(errorPr, '', false)).toBe(false);
expect(h.buildExists(errorPr, '', true)).toBe(true);
prUpdated(+errorPr).
then(h.verifyResponse(500, /Test/)).
then(() => {
// PR visibility should not have been updated.
expect(h.buildExists(errorPr, '', false)).toBe(false);
expect(h.buildExists(errorPr, '', true)).toBe(true);
}).
then(done);
});
it('should reject a request if updating visibility fails', done => {
// One way to cause an error is to have both a public and a hidden directory for the same PR.
h.createDummyBuild(pr9, sha9, false);
h.createDummyBuild(pr9, sha9, true);
const hiddenPrDir = h.getPrDir(pr9, false);
const publicPrDir = h.getPrDir(pr9, true);
const bodyRegex = new RegExp(`Request to move '${hiddenPrDir}' to existing directory '${publicPrDir}'`);
expect(h.buildExists(pr9, '', false)).toBe(true);
expect(h.buildExists(pr9, '', true)).toBe(true);
prUpdated(+pr9).
then(h.verifyResponse(409, bodyRegex)).
then(() => {
// PR visibility should not have been updated.
expect(h.buildExists(pr9, '', false)).toBe(true);
expect(h.buildExists(pr9, '', true)).toBe(true);
}).
then(done);
});
});
}));

View File

@ -1,17 +1,31 @@
// Imports
import {GithubPullRequests} from '../common/github-pull-requests';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
import {UploadError} from './upload-error';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier';
import {UploadError} from '../upload-server/upload-error';
import * as c from './constants';
// Run
// TODO(gkalpak): Add e2e tests to cover these interactions as well.
GithubPullRequests.prototype.addComment = () => Promise.resolve();
BuildVerifier.prototype.getPrIsTrusted = (pr: number) => {
switch (pr) {
case c.BV_getPrIsTrusted_error:
// For e2e tests, fake an error.
return Promise.reject('Test');
case c.BV_getPrIsTrusted_notTrusted:
// For e2e tests, fake an untrusted PR (`false`).
return Promise.resolve(false);
default:
// For e2e tests, default to trusted PRs (`true`).
return Promise.resolve(true);
}
};
BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
switch (authHeader) {
case 'FAKE_VERIFICATION_ERROR':
case c.BV_verify_error:
// For e2e tests, fake a verification error.
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
case 'FAKE_VERIFIED_NOT_TRUSTED':
case c.BV_verify_verifiedNotTrusted:
// For e2e tests, fake a `verifiedNotTrusted` verification status.
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
default:
@ -21,4 +35,4 @@ BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
};
// tslint:disable-next-line: no-var-requires
require('./index');
require('../upload-server/index');

View File

@ -1,6 +1,7 @@
// Imports
import * as fs from 'fs';
import * as path from 'path';
import * as c from './constants';
import {CmdResult, helper as h} from './helper';
// Tests
@ -25,13 +26,13 @@ describe('upload-server (on HTTP)', () => {
it('should disallow non-GET requests', done => {
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Unsupported method/;
const bodyRegex = /^Unknown resource/;
Promise.all([
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX POST ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});
@ -63,7 +64,7 @@ describe('upload-server (on HTTP)', () => {
it('should reject requests for which the PR verification fails', done => {
const headers = `--header "Authorization: FAKE_VERIFICATION_ERROR" ${xFileHeader}`;
const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`;
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
@ -107,7 +108,7 @@ describe('upload-server (on HTTP)', () => {
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
const authorizationHeader2 = isPublic ?
authorizationHeader : '--header "Authorization: FAKE_VERIFIED_NOT_TRUSTED"';
authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`;
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
@ -373,27 +374,194 @@ describe('upload-server (on HTTP)', () => {
});
describe(`${host}/pr-updated`, () => {
const url = `http://${host}/pr-updated`;
// Helpers
const curl = (payload?: {number: number, action?: string}) => {
const payloadStr = payload && JSON.stringify(payload) || '';
return `curl -iLX POST --header "Content-Type: application/json" --data '${payloadStr}' ${url}`;
};
it('should disallow non-POST requests', done => {
const bodyRegex = /^Unknown resource in request/;
Promise.all([
h.runCmd(`curl -iLX GET ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PUT ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PATCH ${url}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX DELETE ${url}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});
it('should respond with 400 for requests without a payload', done => {
const bodyRegex = /^Missing or empty 'number' field in request/;
h.runCmd(curl()).
then(h.verifyResponse(400, bodyRegex)).
then(done);
});
it('should respond with 400 for requests without a \'number\' field', done => {
const bodyRegex = /^Missing or empty 'number' field in request/;
Promise.all([
h.runCmd(curl({} as any)).then(h.verifyResponse(400, bodyRegex)),
h.runCmd(curl({number: null} as any)).then(h.verifyResponse(400, bodyRegex)),
]).then(done);
});
it('should reject requests for which checking the PR visibility fails', done => {
h.runCmd(curl({number: c.BV_getPrIsTrusted_error})).
then(h.verifyResponse(500, /Test/)).
then(done);
});
it('should respond with 404 for unknown paths', done => {
const mockPayload = JSON.stringify({number: +pr});
const cmdPrefix = `curl -iLX POST --data "${mockPayload}" http://${host}`;
Promise.all([
h.runCmd(`${cmdPrefix}/foo/pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foo-pr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/foonpr-updated`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated/foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updated-foo`).then(h.verifyResponse(404)),
h.runCmd(`${cmdPrefix}/pr-updatednfoo`).then(h.verifyResponse(404)),
]).then(done);
});
it('should do nothing if PR\'s visibility is already up-to-date', done => {
const publicPr = pr;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
const checkVisibilities = () => {
// Public build is already public.
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
// Hidden build is already hidden.
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
};
h.createDummyBuild(publicPr, sha9, true);
h.createDummyBuild(hiddenPr, sha9, false);
checkVisibilities();
Promise.
all([
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
]).
// Visibilities should not have changed, because the specified action could not have triggered a change.
then(checkVisibilities).
then(done);
});
it('should do nothing if \'action\' implies no visibility change', done => {
const publicPr = pr;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
const checkVisibilities = () => {
// Public build is hidden atm.
expect(h.buildExists(publicPr, '', false)).toBe(true);
expect(h.buildExists(publicPr, '', true)).toBe(false);
// Hidden build is public atm.
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
};
h.createDummyBuild(publicPr, sha9, false);
h.createDummyBuild(hiddenPr, sha9, true);
checkVisibilities();
Promise.
all([
h.runCmd(curl({number: +publicPr, action: 'foo'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'foo'})).then(h.verifyResponse(200)),
]).
// Visibilities should not have changed, because the specified action could not have triggered a change.
then(checkVisibilities).
then(done);
});
describe('when the visiblity has changed', () => {
const publicPr = pr;
const hiddenPr = String(c.BV_getPrIsTrusted_notTrusted);
beforeEach(() => {
// Create initial PR builds with opposite visibilities as the ones that will be reported:
// - The now public PR was previously hidden.
// - The now hidden PR was previously public.
h.createDummyBuild(publicPr, sha9, false);
h.createDummyBuild(hiddenPr, sha9, true);
expect(h.buildExists(publicPr, '', false)).toBe(true);
expect(h.buildExists(publicPr, '', true)).toBe(false);
expect(h.buildExists(hiddenPr, '', false)).toBe(false);
expect(h.buildExists(hiddenPr, '', true)).toBe(true);
});
afterEach(() => {
// Expect PRs' visibility to have been updated:
// - The public PR should be actually public (previously it was hidden).
// - The hidden PR should be actually hidden (previously it was public).
expect(h.buildExists(publicPr, '', false)).toBe(false);
expect(h.buildExists(publicPr, '', true)).toBe(true);
expect(h.buildExists(hiddenPr, '', false)).toBe(true);
expect(h.buildExists(hiddenPr, '', true)).toBe(false);
h.deletePrDir(publicPr, true);
h.deletePrDir(hiddenPr, false);
});
it('should update the PR\'s visibility (action: undefined)', done => {
Promise.all([
h.runCmd(curl({number: +publicPr})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr})).then(h.verifyResponse(200)),
]).then(done);
});
it('should update the PR\'s visibility (action: labeled)', done => {
Promise.all([
h.runCmd(curl({number: +publicPr, action: 'labeled'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'labeled'})).then(h.verifyResponse(200)),
]).then(done);
});
it('should update the PR\'s visibility (action: unlabeled)', done => {
Promise.all([
h.runCmd(curl({number: +publicPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
h.runCmd(curl({number: +hiddenPr, action: 'unlabeled'})).then(h.verifyResponse(200)),
]).then(done);
});
});
});
describe(`${host}/*`, () => {
it('should respond with 404 for GET requests to unknown URLs', done => {
it('should respond with 404 for requests to unknown URLs', done => {
const bodyRegex = /^Unknown resource/;
Promise.all([
h.runCmd(`curl -iL http://${host}/index.html`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL http://${host}/`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iL http://${host}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});
it('should respond with 405 for non-GET requests to any URL', done => {
const bodyRegex = /^Unsupported method/;
Promise.all([
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(405, bodyRegex)),
h.runCmd(`curl -iLX PUT http://${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX POST http://${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX PATCH http://${host}`).then(h.verifyResponse(404, bodyRegex)),
h.runCmd(`curl -iLX DELETE http://${host}`).then(h.verifyResponse(404, bodyRegex)),
]).then(done);
});

View File

@ -20,12 +20,14 @@
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
},
"dependencies": {
"body-parser": "^1.17.2",
"express": "^4.14.1",
"jasmine": "^2.5.3",
"jsonwebtoken": "^7.3.0",
"shelljs": "^0.7.6"
},
"devDependencies": {
"@types/body-parser": "^1.16.4",
"@types/express": "^4.0.35",
"@types/jasmine": "^2.5.43",
"@types/jsonwebtoken": "^7.2.0",

View File

@ -43,178 +43,25 @@ describe('BuildCreator', () => {
});
describe('changePrVisibility()', () => {
let bcEmitSpy: jasmine.Spy;
let bcExistsSpy: jasmine.Spy;
let bcListShasByDate: jasmine.Spy;
let shellMvSpy: jasmine.Spy;
beforeEach(() => {
bcEmitSpy = spyOn(bc, 'emit');
bcExistsSpy = spyOn(bc as any, 'exists');
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
shellMvSpy = spyOn(shell, 'mv');
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
bcListShasByDate.and.returnValue([]);
});
it('should return a promise', done => {
const promise = bc.changePrVisibility(pr, true);
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `extractArchive()`.
expect(promise).toEqual(jasmine.any(Promise));
});
[true, false].forEach(makePublic => {
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
it('should rename the directory', done => {
bc.changePrVisibility(pr, makePublic).
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
then(done);
});
it('should emit a ChangedPrVisibilityEvent on success', done => {
let emitted = false;
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
expect(type).toBe(ChangedPrVisibilityEvent.type);
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
expect(evt.pr).toBe(+pr);
expect(evt.shas).toEqual(jasmine.any(Array));
expect(evt.isPublic).toBe(makePublic);
emitted = true;
});
bc.changePrVisibility(pr, makePublic).
then(() => expect(emitted).toBe(true)).
then(done);
});
it('should include all shas in the emitted event', done => {
const shas = ['foo', 'bar', 'baz'];
let emitted = false;
bcListShasByDate.and.returnValue(Promise.resolve(shas));
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
expect(type).toBe(ChangedPrVisibilityEvent.type);
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
expect(evt.pr).toBe(+pr);
expect(evt.shas).toBe(shas);
expect(evt.isPublic).toBe(makePublic);
emitted = true;
});
bc.changePrVisibility(pr, makePublic).
then(() => expect(emitted).toBe(true)).
then(done);
});
describe('on error', () => {
it('should abort and skip further operations if the old directory does not exist', done => {
bcExistsSpy.and.callFake((dir: string) => dir !== oldPrDir);
bc.changePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 404, `Request to move non-existing directory '${oldPrDir}' to '${newPrDir}'.`);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should abort and skip further operations if the new directory does already exist', done => {
bcExistsSpy.and.returnValue(true);
bc.changePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should abort and skip further operations if it fails to rename the directory', done => {
shellMvSpy.and.throwError('');
bc.changePrVisibility(pr, makePublic).catch(() => {
expect(shellMvSpy).toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should abort and skip further operations if it fails to list the SHAs', done => {
bcListShasByDate.and.throwError('');
bc.changePrVisibility(pr, makePublic).catch(() => {
expect(shellMvSpy).toHaveBeenCalled();
expect(bcListShasByDate).toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should reject with an UploadError', done => {
shellMvSpy.and.callFake(() => { throw 'Test'; });
bc.changePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
done();
});
});
it('should pass UploadError instances unmodified', done => {
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
bc.changePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 543, 'Test');
done();
});
});
});
});
});
describe('create()', () => {
let bcChangePrVisibilitySpy: jasmine.Spy;
let bcEmitSpy: jasmine.Spy;
let bcExistsSpy: jasmine.Spy;
let bcExtractArchiveSpy: jasmine.Spy;
let bcUpdatePrVisibilitySpy: jasmine.Spy;
let shellMkdirSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
beforeEach(() => {
bcChangePrVisibilitySpy = spyOn(bc, 'changePrVisibility');
bcEmitSpy = spyOn(bc, 'emit');
bcExistsSpy = spyOn(bc as any, 'exists');
bcExtractArchiveSpy = spyOn(bc as any, 'extractArchive');
bcUpdatePrVisibilitySpy = spyOn(bc, 'updatePrVisibility');
shellMkdirSpy = spyOn(shell, 'mkdir');
shellRmSpy = spyOn(shell, 'rm');
});
[true, false].forEach(isPublic => {
const otherVisPrDir = isPublic ? hiddenPrDir : publicPrDir;
const prDir = isPublic ? publicPrDir : hiddenPrDir;
const shaDir = isPublic ? publicShaDir : hiddenShaDir;
@ -228,20 +75,12 @@ describe('BuildCreator', () => {
});
it('should not update the PR\'s visibility first if not necessary', done => {
bc.create(pr, sha, archive, isPublic).
then(() => expect(bcChangePrVisibilitySpy).not.toHaveBeenCalled()).
then(done);
});
it('should update the PR\'s visibility first if necessary', done => {
bcChangePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
bcExistsSpy.and.callFake((dir: string) => dir === otherVisPrDir);
bcUpdatePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
bc.create(pr, sha, archive, isPublic).
then(() => {
expect(bcChangePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
expect(shellMkdirSpy).toHaveBeenCalled();
}).
then(done);
@ -286,7 +125,6 @@ describe('BuildCreator', () => {
beforeEach(() => {
existsValues = {
[otherVisPrDir]: false,
[prDir]: false,
[shaDir]: false,
};
@ -297,14 +135,12 @@ describe('BuildCreator', () => {
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
const mockError = new UploadError(543, 'Test');
existsValues[otherVisPrDir] = true;
bcChangePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
bc.create(pr, sha, archive, isPublic).catch(err => {
expect(err).toBe(mockError);
expect(bcExistsSpy).toHaveBeenCalledTimes(1);
expect(bcExistsSpy).not.toHaveBeenCalled();
expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
@ -327,8 +163,10 @@ describe('BuildCreator', () => {
it('should detect existing build directory after visibility change', done => {
existsValues[otherVisPrDir] = true;
bcChangePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
bcUpdatePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
expect(bcExistsSpy(prDir)).toBe(false);
expect(bcExistsSpy(shaDir)).toBe(false);
bc.create(pr, sha, archive, isPublic).catch(err => {
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
@ -406,6 +244,190 @@ describe('BuildCreator', () => {
});
describe('updatePrVisibility()', () => {
let bcEmitSpy: jasmine.Spy;
let bcExistsSpy: jasmine.Spy;
let bcListShasByDate: jasmine.Spy;
let shellMvSpy: jasmine.Spy;
beforeEach(() => {
bcEmitSpy = spyOn(bc, 'emit');
bcExistsSpy = spyOn(bc as any, 'exists');
bcListShasByDate = spyOn(bc as any, 'listShasByDate');
shellMvSpy = spyOn(shell, 'mv');
bcExistsSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
bcListShasByDate.and.returnValue([]);
});
it('should return a promise', done => {
const promise = bc.updatePrVisibility(pr, true);
promise.then(done); // Do not complete the test (and release the spies) synchronously
// to avoid running the actual `extractArchive()`.
expect(promise).toEqual(jasmine.any(Promise));
});
[true, false].forEach(makePublic => {
const oldPrDir = makePublic ? hiddenPrDir : publicPrDir;
const newPrDir = makePublic ? publicPrDir : hiddenPrDir;
it('should rename the directory', done => {
bc.updatePrVisibility(pr, makePublic).
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
then(done);
});
describe('when the visibility is updated', () => {
it('should resolve to true', done => {
bc.updatePrVisibility(pr, makePublic).
then(result => expect(result).toBe(true)).
then(done);
});
it('should rename the directory', done => {
bc.updatePrVisibility(pr, makePublic).
then(() => expect(shellMvSpy).toHaveBeenCalledWith(oldPrDir, newPrDir)).
then(done);
});
it('should emit a ChangedPrVisibilityEvent on success', done => {
let emitted = false;
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
expect(type).toBe(ChangedPrVisibilityEvent.type);
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
expect(evt.pr).toBe(+pr);
expect(evt.shas).toEqual(jasmine.any(Array));
expect(evt.isPublic).toBe(makePublic);
emitted = true;
});
bc.updatePrVisibility(pr, makePublic).
then(() => expect(emitted).toBe(true)).
then(done);
});
it('should include all shas in the emitted event', done => {
const shas = ['foo', 'bar', 'baz'];
let emitted = false;
bcListShasByDate.and.returnValue(Promise.resolve(shas));
bcEmitSpy.and.callFake((type: string, evt: ChangedPrVisibilityEvent) => {
expect(bcListShasByDate).toHaveBeenCalledWith(newPrDir);
expect(type).toBe(ChangedPrVisibilityEvent.type);
expect(evt).toEqual(jasmine.any(ChangedPrVisibilityEvent));
expect(evt.pr).toBe(+pr);
expect(evt.shas).toBe(shas);
expect(evt.isPublic).toBe(makePublic);
emitted = true;
});
bc.updatePrVisibility(pr, makePublic).
then(() => expect(emitted).toBe(true)).
then(done);
});
});
it('should do nothing if the visibility is already up-to-date', done => {
bcExistsSpy.and.callFake((dir: string) => dir === newPrDir);
bc.updatePrVisibility(pr, makePublic).
then(result => {
expect(result).toBe(false);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
}).
then(done);
});
it('should do nothing if the PR directory does not exist', done => {
bcExistsSpy.and.returnValue(false);
bc.updatePrVisibility(pr, makePublic).
then(result => {
expect(result).toBe(false);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
}).
then(done);
});
describe('on error', () => {
it('should abort and skip further operations if both directories exist', done => {
bcExistsSpy.and.returnValue(true);
bc.updatePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 409, `Request to move '${oldPrDir}' to existing directory '${newPrDir}'.`);
expect(shellMvSpy).not.toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should abort and skip further operations if it fails to rename the directory', done => {
shellMvSpy.and.throwError('');
bc.updatePrVisibility(pr, makePublic).catch(() => {
expect(shellMvSpy).toHaveBeenCalled();
expect(bcListShasByDate).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should abort and skip further operations if it fails to list the SHAs', done => {
bcListShasByDate.and.throwError('');
bc.updatePrVisibility(pr, makePublic).catch(() => {
expect(shellMvSpy).toHaveBeenCalled();
expect(bcListShasByDate).toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
done();
});
});
it('should reject with an UploadError', done => {
shellMvSpy.and.callFake(() => { throw 'Test'; });
bc.updatePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 500, `Error while making PR ${pr} ${makePublic ? 'public' : 'hidden'}.\nTest`);
done();
});
});
it('should pass UploadError instances unmodified', done => {
shellMvSpy.and.callFake(() => { throw new UploadError(543, 'Test'); });
bc.updatePrVisibility(pr, makePublic).catch(err => {
expectToBeUploadError(err, 543, 'Test');
done();
});
});
});
});
});
// Protected methods
describe('exists()', () => {

View File

@ -258,12 +258,12 @@ describe('uploadServerFactory', () => {
});
it('should respond with 405 for non-GET requests', done => {
it('should respond with 404 for non-GET requests', done => {
verifyRequests([
agent.put(`/create-build/${pr}/${sha}`).expect(405),
agent.post(`/create-build/${pr}/${sha}`).expect(405),
agent.patch(`/create-build/${pr}/${sha}`).expect(405),
agent.delete(`/create-build/${pr}/${sha}`).expect(405),
agent.put(`/create-build/${pr}/${sha}`).expect(404),
agent.post(`/create-build/${pr}/${sha}`).expect(404),
agent.patch(`/create-build/${pr}/${sha}`).expect(404),
agent.delete(`/create-build/${pr}/${sha}`).expect(404),
], done);
});
@ -418,12 +418,12 @@ describe('uploadServerFactory', () => {
});
it('should respond with 405 for non-GET requests', done => {
it('should respond with 404 for non-GET requests', done => {
verifyRequests([
agent.put('/health-check').expect(405),
agent.post('/health-check').expect(405),
agent.patch('/health-check').expect(405),
agent.delete('/health-check').expect(405),
agent.put('/health-check').expect(404),
agent.post('/health-check').expect(404),
agent.patch('/health-check').expect(404),
agent.delete('/health-check').expect(404),
], done);
});
@ -442,11 +442,141 @@ describe('uploadServerFactory', () => {
});
describe('GET *', () => {
describe('POST /pr-updated', () => {
const pr = '9';
const url = '/pr-updated';
let bvGetPrIsTrustedSpy: jasmine.Spy;
let bcUpdatePrVisibilitySpy: jasmine.Spy;
// Helpers
const createRequest = (num: number, action?: string) =>
agent.post(url).send({number: num, action});
beforeEach(() => {
bvGetPrIsTrustedSpy = spyOn(buildVerifier, 'getPrIsTrusted');
bcUpdatePrVisibilitySpy = spyOn(buildCreator, 'updatePrVisibility');
});
it('should respond with 404 for non-POST requests', done => {
verifyRequests([
agent.get(url).expect(404),
agent.put(url).expect(404),
agent.patch(url).expect(404),
agent.delete(url).expect(404),
], done);
});
it('should respond with 400 for requests without a payload', done => {
const responseBody = `Missing or empty 'number' field in request: POST ${url} {}`;
const request1 = agent.post(url);
const request2 = agent.post(url).send();
verifyRequests([
request1.expect(400, responseBody),
request2.expect(400, responseBody),
], done);
});
it('should respond with 400 for requests without a \'number\' field', done => {
const responseBodyPrefix = `Missing or empty 'number' field in request: POST ${url}`;
const request1 = agent.post(url).send({});
const request2 = agent.post(url).send({number: null});
verifyRequests([
request1.expect(400, `${responseBodyPrefix} {}`),
request2.expect(400, `${responseBodyPrefix} {"number":null}`),
], done);
});
it('should call \'BuildVerifier#gtPrIsTrusted()\' with the correct arguments', done => {
const req = createRequest(+pr);
promisifyRequest(req).
then(() => expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9)).
then(done, done.fail);
});
it('should propagate errors from BuildVerifier', done => {
bvGetPrIsTrustedSpy.and.callFake(() => Promise.reject('Test'));
const req = createRequest(+pr).expect(500, 'Test');
promisifyRequest(req).
then(() => {
expect(bvGetPrIsTrustedSpy).toHaveBeenCalledWith(9);
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
}).
then(done, done.fail);
});
it('should call \'BuildCreator#updatePrVisibility()\' with the correct arguments', done => {
bvGetPrIsTrustedSpy.and.callFake((pr2: number) => Promise.resolve(pr2 === 42));
const req1 = createRequest(24);
const req2 = createRequest(42);
Promise.all([
promisifyRequest(req1).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('24', false)),
promisifyRequest(req2).then(() => expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith('42', true)),
]).then(done, done.fail);
});
it('should propagate errors from BuildCreator', done => {
bcUpdatePrVisibilitySpy.and.callFake(() => Promise.reject('Test'));
const req = createRequest(+pr).expect(500, 'Test');
verifyRequests([req], done);
});
describe('on success', () => {
it('should respond with 200 (action: undefined)', done => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num).expect(200, http.STATUS_CODES[200]));
verifyRequests(reqs, done);
});
it('should respond with 200 (action: labeled)', done => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'labeled').expect(200, http.STATUS_CODES[200]));
verifyRequests(reqs, done);
});
it('should respond with 200 (action: unlabeled)', done => {
bvGetPrIsTrustedSpy.and.returnValues(Promise.resolve(true), Promise.resolve(false));
const reqs = [4, 2].map(num => createRequest(num, 'unlabeled').expect(200, http.STATUS_CODES[200]));
verifyRequests(reqs, done);
});
it('should respond with 200 (and do nothing) if \'action\' implies no visibility change', done => {
const promises = ['foo', 'notlabeled'].
map(action => createRequest(+pr, action).expect(200, http.STATUS_CODES[200])).
map(promisifyRequest);
Promise.all(promises).
then(() => {
expect(bvGetPrIsTrustedSpy).not.toHaveBeenCalled();
expect(bcUpdatePrVisibilitySpy).not.toHaveBeenCalled();
}).
then(done, done.fail);
});
it('should respond with 404', done => {
const responseBody = 'Unknown resource in request: GET /some/url';
verifyRequests([agent.get('/some/url').expect(404, responseBody)], done);
});
});
@ -454,14 +584,15 @@ describe('uploadServerFactory', () => {
describe('ALL *', () => {
it('should respond with 405', done => {
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
it('should respond with 404', done => {
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
verifyRequests([
agent.put('/some/url').expect(405, responseFor('put')),
agent.post('/some/url').expect(405, responseFor('post')),
agent.patch('/some/url').expect(405, responseFor('patch')),
agent.delete('/some/url').expect(405, responseFor('delete')),
agent.get('/some/url').expect(404, responseFor('get')),
agent.put('/some/url').expect(404, responseFor('put')),
agent.post('/some/url').expect(404, responseFor('post')),
agent.patch('/some/url').expect(404, responseFor('patch')),
agent.delete('/some/url').expect(404, responseFor('delete')),
], done);
});

View File

@ -2,13 +2,20 @@
# yarn lockfile v1
"@types/body-parser@^1.16.4":
version "1.16.4"
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.4.tgz#96f3660e6f88a677fee7250f5a5e6d6bda3c76bb"
dependencies:
"@types/express" "*"
"@types/node" "*"
"@types/express-serve-static-core@*":
version "4.0.48"
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.0.48.tgz#b4fa06b0fce282e582b4535ff7fac85cc90173e9"
dependencies:
"@types/node" "*"
"@types/express@^4.0.35":
"@types/express@*", "@types/express@^4.0.35":
version "4.0.36"
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.0.36.tgz#14eb47de7ecb10319f0a2fb1cf971aa8680758c2"
dependencies:
@ -236,6 +243,21 @@ block-stream@*:
dependencies:
inherits "~2.0.0"
body-parser@^1.17.2:
version "1.17.2"
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.17.2.tgz#f8892abc8f9e627d42aedafbca66bf5ab99104ee"
dependencies:
bytes "2.4.0"
content-type "~1.0.2"
debug "2.6.7"
depd "~1.1.0"
http-errors "~1.6.1"
iconv-lite "0.4.15"
on-finished "~2.3.0"
qs "6.4.0"
raw-body "~2.2.0"
type-is "~1.6.15"
boom@2.x.x:
version "2.10.1"
resolved "https://registry.yarnpkg.com/boom/-/boom-2.10.1.tgz#39c8918ceff5799f83f9492a848f625add0c766f"
@ -273,6 +295,10 @@ buffer-equal-constant-time@1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz#f8e71132f7ffe6e01a5c9697a4c6f3e48d5cc819"
bytes@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/bytes/-/bytes-2.4.0.tgz#7d97196f9d5baf7f6935e25985549edd2a6c2339"
caller-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/caller-path/-/caller-path-0.1.0.tgz#94085ef63581ecd3daa92444a8fe94e82577751f"
@ -1158,6 +1184,10 @@ http-signature@~1.1.0:
jsprim "^1.2.2"
sshpk "^1.7.0"
iconv-lite@0.4.15:
version "0.4.15"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.15.tgz#fe265a218ac6a57cfe854927e9d04c19825eddeb"
ignore-by-default@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
@ -1958,6 +1988,14 @@ range-parser@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e"
raw-body@~2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.2.0.tgz#994976cf6a5096a41162840492f0bdc5d6e7fb96"
dependencies:
bytes "2.4.0"
iconv-lite "0.4.15"
unpipe "1.0.0"
rc@^1.0.1, rc@^1.1.6, rc@^1.1.7:
version "1.2.1"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.1.tgz#2e03e8e42ee450b8cb3dce65be1bf8974e1dfd95"
@ -2477,7 +2515,7 @@ unique-string@^1.0.0:
dependencies:
crypto-random-string "^1.0.0"
unpipe@~1.0.0:
unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"

View File

@ -21,7 +21,7 @@ appName=aio-upload-server-test
if [[ "$1" == "stop" ]]; then
pm2 delete $appName
else
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server/index-test.js \
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-upload-server.js \
--log /var/log/aio/upload-server-test.log \
--name $appName \
--no-autorestart \

View File

@ -3,10 +3,9 @@
TODO (gkalpak): Add docs. Mention:
- Travis' JWT addon (+ limitations).
Relevant files: `.travis.yml`
Relevant files: `.travis.yml`, `scripts/ci/env.sh`
- Testing on CI.
Relevant files: `ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
- Preverifying on CI.
Relevant files: `ci/deploy.sh`, `aio/aio-builds-setup/scripts/travis-preverify-pr.sh`
Relevant files: `scripts/ci/test-aio.sh`, `aio/aio-builds-setup/scripts/test.sh`
- Deploying from CI.
Relevant files: `ci/deploy.sh`, `aio/scripts/deploy-preview.sh`
Relevant files: `scripts/ci/deploy.sh`, `aio/scripts/deploy-preview.sh`,
`aio/scripts/deploy-to-firebase.sh`

View File

@ -80,13 +80,31 @@ More info on the possible HTTP status codes and their meaning can be found
[here](overview--http-status-codes.md).
### Updating PR visibility
- nginx receives a natification that a PR has been updated and passes it through to the
upload-server. This could, for example, be sent by a GitHub webhook every time a PR's labels
change.
E.g.: `ngbuilds.io/pr-updated` (payload: `{"number":<PR>,"action":"labeled"}`)
- The request contains the PR number (as `number`) and optionally the action that triggered the
request (as `action`) in the payload.
- The upload-server verifies the payload and determines whether the `action` (if specified) could
have led to PR visibility changes. Only requests that omit the `action` field altogether or
specify an action that can affect visibility are further processed.
(Currently, the only actions that are considered capable of affecting visibility are `labeled` and
`unlabeled`.)
- The upload-server re-checks and if necessary updates the PR's visibility.
More info on the possible HTTP status codes and their meaning can be found
[here](overview--http-status-codes.md).
### Serving build artifacts
- nginx receives a request for an uploaded resource on a subdomain corresponding to the PR and SHA.
E.g.: `pr<PR>-<SHA>.ngbuilds.io/path/to/resource`
- nginx maps the subdomain to the correct sub-directory and serves the resource.
E.g.: `/<PR>/<SHA>/path/to/resource`
Again, more info on the possible HTTP status codes and their meaning can be found
More info on the possible HTTP status codes and their meaning can be found
[here](overview--http-status-codes.md).

View File

@ -42,10 +42,6 @@ with a bried explanation of what they mean:
- **403 (Forbidden)**:
Unable to verify build (e.g. invalid JWT token, or unable to talk to 3rd-party APIs, etc).
- **404 (Not Found)**:
Tried to change PR visibility but the source directory did not exist.
(Currently, this can only happen as a rare race condition during build deployment.)
- **405 (Method Not Allowed)**:
Request method other than POST.
@ -57,6 +53,28 @@ with a bried explanation of what they mean:
Payload larger than size specified in `AIO_UPLOAD_MAX_SIZE`.
## `https://ngbuilds.io/health-check`
- **200 (OK)**:
The server is healthy (i.e. up and running and processing requests).
## `https://ngbuilds.io/pr-updated`
- **200 (OK)**:
Request processed successfully. Processing may or may not have resulted in further actions.
- **400 (Bad Request)**:
No payload or no `number` field in payload.
- **405 (Method Not Allowed)**:
Request method other than POST.
- **409 (Conflict)**:
Request to overwrite existing directory (i.e. directories for both visibilities exist).
(Normally, this should not happen.)
## `https://*.ngbuilds.io/*`
- **404 (Not Found)**:

View File

@ -16,13 +16,6 @@ available:
Can be used for running the tests for `<aio-builds-setup-dir>/dockerbuild/scripts-js/`. This is
useful for CI integration. See [here](misc--integrate-with-ci.md) for more info.
- `travis-preverify-pr.sh`:
Can be used for "pre-verifying" a PR before uploading the artifacts to the server. It checks
whether the author of the PR is a member of one of the specified GitHub teams (therefore allowed
to upload build artifacts) or the PR has the specified "trusted PR" label (meaning it has been
manually verified by a trusted member). This is useful for CI integration.
See [here](misc--integrate-with-ci.md) for more info.
- `update-preview-server.sh`:
Can be used for updating the docker container (and image) based on the latest changes checked out
from a git repository. See [here](vm-setup--update-docker-container.md) for more info.

View File

@ -96,7 +96,7 @@ This section describes how each of the aforementioned sub-tasks is accomplished:
5. **Deploy the artifacts to the corresponding PR's directory.**
With the preceeding steps, we have verified that the uploaded artifacts have been uploaded by
With the preceding steps, we have verified that the uploaded artifacts have been uploaded by
Travis. Additionally, we have determined whether the PR can be trusted to have its previews
publicly accessible or whether further verification is necessary. The artifacts will be stored to
the PR's directory, but will not be publicly accessible unless the PR has been verified.

View File

@ -1,26 +0,0 @@
#!/bin/bash
set -eux -o pipefail
# Set up env
source "`dirname $0`/_env.sh"
# Build `scripts-js/`
(
cd "$SCRIPTS_JS_DIR"
yarn install
yarn build
)
# Preverify PR
AIO_GITHUB_ORGANIZATION="angular" \
AIO_GITHUB_TEAM_SLUGS="angular-core,aio-contributors" \
AIO_GITHUB_TOKEN=$(echo ${GITHUB_TEAM_MEMBERSHIP_CHECK_KEY} | rev) \
AIO_REPO_SLUG=$TRAVIS_REPO_SLUG \
AIO_TRUSTED_PR_LABEL="aio: preview" \
AIO_PREVERIFY_PR=$TRAVIS_PULL_REQUEST \
node "$SCRIPTS_JS_DIR/dist/lib/upload-server/index-preverify-pr"
# Exit codes:
# - 0: The PR can be automatically trusted (i.e. author belongs to trusted team or PR has the "trusted PR" label).
# - 1: An error occurred.
# - 2: The PR cannot be automatically trusted.

View File

@ -14,13 +14,13 @@
<h1>Example Snippets</h1>
<!-- #docregion ngClass -->
<div [ngClass]="{active: isActive}">
<div [ngClass]="{'active': isActive}">
<!-- #enddocregion ngClass -->
[ngClass] active
</div>
<!-- #docregion ngClass -->
<div [ngClass]="{active: isActive,
shazam: isImportant}">
<div [ngClass]="{'active': isActive,
'shazam': isImportant}">
<!-- #enddocregion ngClass -->
[ngClass] active and boldly important
</div>
@ -57,7 +57,7 @@
<p></p>
<!-- #docregion ngStyle -->
<div [ngStyle]="{color: colorPreference}">
<div [ngStyle]="{'color': colorPreference}">
<!-- #enddocregion ngStyle -->
color preference #1
</div>

View File

@ -6,7 +6,7 @@ import { Hero } from './hero';
const HEROES = [
new Hero('Windstorm', 'Weather mastery'),
new Hero('Mr. Nice', 'Killing them with kindness'),
new Hero('Magneta', 'Manipulates metalic objects')
new Hero('Magneta', 'Manipulates metallic objects')
];
@Injectable()

View File

@ -9,6 +9,6 @@ describe('cli-quickstart App', () => {
it('should display message saying app works', () => {
let pageTitle = element(by.css('app-root h1')).getText();
expect(pageTitle).toEqual('My First Angular App');
expect(pageTitle).toEqual('Welcome to My First Angular App!!');
});
});

View File

@ -9,6 +9,6 @@ describe('my-app App', function() {
it('should display message saying app works', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('app works!');
expect(page.getParagraphText()).toEqual('Welcome to app!!');
});
});

View File

@ -1,18 +1,12 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2016"
],
"outDir": "../dist/out-tsc-e2e",
"outDir": "../out-tsc/e2e",
"module": "commonjs",
"target": "es6",
"types":[
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}

View File

@ -1,3 +1,20 @@
<h1>
{{title}}
</h1>
<!--The content below is only a placeholder and can be replaced.-->
<div style="text-align:center">
<h1>
Welcome to {{title}}!!
</h1>
<img width="300" alt="Angular logo" src="data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0idXRmLTgiPz4NCjwhLS0gR2VuZXJhdG9yOiBBZG9iZSBJbGx1c3RyYXRvciAxOS4xLjAsIFNWRyBFeHBvcnQgUGx1Zy1JbiAuIFNWRyBWZXJzaW9uOiA2LjAwIEJ1aWxkIDApICAtLT4NCjxzdmcgdmVyc2lvbj0iMS4xIiBpZD0iTGF5ZXJfMSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayIgeD0iMHB4IiB5PSIwcHgiDQoJIHZpZXdCb3g9IjAgMCAyNTAgMjUwIiBzdHlsZT0iZW5hYmxlLWJhY2tncm91bmQ6bmV3IDAgMCAyNTAgMjUwOyIgeG1sOnNwYWNlPSJwcmVzZXJ2ZSI+DQo8c3R5bGUgdHlwZT0idGV4dC9jc3MiPg0KCS5zdDB7ZmlsbDojREQwMDMxO30NCgkuc3Qxe2ZpbGw6I0MzMDAyRjt9DQoJLnN0MntmaWxsOiNGRkZGRkY7fQ0KPC9zdHlsZT4NCjxnPg0KCTxwb2x5Z29uIGNsYXNzPSJzdDAiIHBvaW50cz0iMTI1LDMwIDEyNSwzMCAxMjUsMzAgMzEuOSw2My4yIDQ2LjEsMTg2LjMgMTI1LDIzMCAxMjUsMjMwIDEyNSwyMzAgMjAzLjksMTg2LjMgMjE4LjEsNjMuMiAJIi8+DQoJPHBvbHlnb24gY2xhc3M9InN0MSIgcG9pbnRzPSIxMjUsMzAgMTI1LDUyLjIgMTI1LDUyLjEgMTI1LDE1My40IDEyNSwxNTMuNCAxMjUsMjMwIDEyNSwyMzAgMjAzLjksMTg2LjMgMjE4LjEsNjMuMiAxMjUsMzAgCSIvPg0KCTxwYXRoIGNsYXNzPSJzdDIiIGQ9Ik0xMjUsNTIuMUw2Ni44LDE4Mi42aDBoMjEuN2gwbDExLjctMjkuMmg0OS40bDExLjcsMjkuMmgwaDIxLjdoMEwxMjUsNTIuMUwxMjUsNTIuMUwxMjUsNTIuMUwxMjUsNTIuMQ0KCQlMMTI1LDUyLjF6IE0xNDIsMTM1LjRIMTA4bDE3LTQwLjlMMTQyLDEzNS40eiIvPg0KPC9nPg0KPC9zdmc+DQo=">
</div>
<h2>Here are some links to help you start: </h2>
<ul>
<li>
<h2><a target="_blank" href="https://angular.io/tutorial">Tour of Heroes</a></h2>
</li>
<li>
<h2><a target="_blank" href="https://github.com/angular/angular-cli/wiki">CLI Documentation</a></h2>
</li>
<li>
<h2><a target="_blank" href="http://angularjs.blogspot.ca/">Angular blog</a></h2>
</li>
</ul>

View File

@ -17,16 +17,16 @@ describe('AppComponent', () => {
expect(app).toBeTruthy();
}));
it(`should have as title 'app works!'`, async(() => {
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app works!');
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('app works!');
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!!');
}));
});

View File

@ -1,7 +1,5 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { AppComponent } from './app.component';
@ -10,9 +8,7 @@ import { AppComponent } from './app.component';
AppComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule
BrowserModule
],
providers: [],
bootstrap: [AppComponent]

View File

@ -1,5 +1,5 @@
<!doctype html>
<html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>MyApp</title>
@ -9,6 +9,6 @@
<link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
<app-root>Loading...</app-root>
<app-root></app-root>
</body>
</html>

View File

@ -31,21 +31,21 @@
// import 'core-js/es6/array';
// import 'core-js/es6/regexp';
// import 'core-js/es6/map';
// import 'core-js/es6/weak-map';
// import 'core-js/es6/set';
/** IE10 and IE11 requires the following for NgClass support on SVG elements */
// import 'classlist.js'; // Run `npm install --save classlist.js`.
/** IE10 and IE11 requires the following to support `@angular/animation`. */
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
/** Evergreen browsers require these. **/
import 'core-js/es6/reflect';
import 'core-js/es7/reflect';
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
/**
* Required to support Web Animations `@angular/animation`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
**/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
@ -66,3 +66,7 @@ import 'zone.js/dist/zone'; // Included with Angular CLI.
* Needed for: All but Chrome, Firefox, Edge, IE11 and Safari 10
*/
// import 'intl'; // Run `npm install --save intl`.
/**
* Need to import at least one locale-data with intl.
*/
// import 'intl/locale-data/jsonp/en';

View File

@ -1,16 +1,7 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2016",
"dom"
],
"outDir": "../out-tsc/app",
"target": "es5",
"module": "es2015",
"baseUrl": "",
"types": []

View File

@ -1,16 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2016"
],
"outDir": "../out-tsc/spec",
"module": "commonjs",
"target": "es6",
"target": "es5",
"baseUrl": "",
"types": [
"jasmine",
@ -21,6 +14,7 @@
"test.ts"
],
"include": [
"**/*.spec.ts"
"**/*.spec.ts",
"**/*.d.ts"
]
}

View File

@ -0,0 +1,5 @@
/* SystemJS module definition */
declare var module: NodeModule;
interface NodeModule {
id: string;
}

View File

@ -2,13 +2,19 @@
"compileOnSave": false,
"compilerOptions": {
"outDir": "./dist/out-tsc",
"baseUrl": "src",
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
],
"lib": [
"es2016"
"es2016",
"dom"
]
}
}

View File

@ -39,7 +39,7 @@ if (!/e2e/.test(location.search)) {
directives.push(CountdownLocalVarParentComponent);
directives.push(CountdownViewChildParentComponent);
} else {
// In e2e test use CUSTOM_ELEMENTS_SCHEMA to supress unknown element errors
// In e2e test use CUSTOM_ELEMENTS_SCHEMA to suppress unknown element errors
schemas.push(CUSTOM_ELEMENTS_SCHEMA);
}

View File

@ -1,5 +1,5 @@
{
"description": "Hierachical Dependency Injection",
"description": "Hierarchical Dependency Injection",
"basePath": "src/",
"files":[
"!**/*.d.ts",

View File

@ -12,7 +12,7 @@ import { UserService } from './user.service';
})
export class TitleComponent {
@Input() subtitle = '';
title = 'Angular Modules';
title = 'NgModules';
// #enddocregion v1
user = '';

View File

@ -85,7 +85,7 @@ describe('Pipes', function () {
return resetEle.click();
})
.then(function() {
expect(flyingHeroesEle.count()).toEqual(2, 'reset should restore orginal flying heroes');
expect(flyingHeroesEle.count()).toEqual(2, 'reset should restore original flying heroes');
});
});

View File

@ -6,7 +6,7 @@ import { Pipe, PipeTransform } from '@angular/core';
* Usage:
* value | exponentialStrength:exponent
* Example:
* {{ 2 | exponentialStrength:10}}
* {{ 2 | exponentialStrength:10 }}
* formats to: 1024
*/
@Pipe({name: 'exponentialStrength'})

View File

@ -16,7 +16,7 @@ import { HeroService } from './hero.service'; // <-- #1 import service
@NgModule({
imports: [
BrowserModule,
ReactiveFormsModule // <-- #2 add to Angular module imports
ReactiveFormsModule // <-- #2 add to @NgModule imports
],
declarations: [
AppComponent,

View File

@ -213,10 +213,10 @@
<h2 id="myUnless">UnlessDirective</h2>
<p>
The condition is currently
<span [ngClass]="{ a: !condition, b: condition, unless: true }">{{condition}}</span>.
<span [ngClass]="{ 'a': !condition, 'b': condition, 'unless': true }">{{condition}}</span>.
<button
(click)="condition = !condition"
[ngClass] = "{ a: condition, b: !condition }" >
[ngClass] = "{ 'a': condition, 'b': !condition }" >
Toggle condition to {{condition ? 'false' : 'true'}}
</button>
</p>

View File

@ -4,7 +4,7 @@ import { Directive } from '@angular/core';
@Directive({
selector: '[tohValidator2]',
host: {
'attr.role': 'button',
'[attr.role]': 'role',
'(mouseenter)': 'onMouseEnter()'
}
})

View File

@ -161,7 +161,7 @@
<!-- #docregion property-binding-syntax-1 -->
<img [src]="heroImageUrl">
<hero-detail [hero]="currentHero"></hero-detail>
<div [ngClass]="{special: isSpecial}"></div>
<div [ngClass]="{'special': isSpecial}"></div>
<!-- #enddocregion property-binding-syntax-1 -->
</div>
<br><br>
@ -496,7 +496,7 @@ bindon-ngModel
<div [ngClass]="isSpecial ? 'special' : ''">This div is special</div>
<div class="bad curly special">Bad curly special</div>
<div [ngClass]="{bad:false, curly:true, special:true}">Curly special</div>
<div [ngClass]="{'bad':false, 'curly':true, 'special':true}">Curly special</div>
<a class="to-toc" href="#toc">top</a>

View File

@ -140,9 +140,9 @@ export class AppComponent implements AfterViewInit, OnInit {
setCurrentClasses() {
// CSS classes: added/removed per current state of component properties
this.currentClasses = {
saveable: this.canSave,
modified: !this.isUnchanged,
special: this.isSpecial
'saveable': this.canSave,
'modified': !this.isUnchanged,
'special': this.isSpecial
};
}
// #enddocregion setClasses

View File

@ -273,7 +273,7 @@ export class InnerCompWithExternalTemplateComponent { }
@Component({
selector: 'bad-template-comp',
templateUrl: './non-existant.html'
templateUrl: './non-existent.html'
})
export class BadTemplateUrlComponent { }

View File

@ -227,7 +227,7 @@ function heroModuleSetup() {
// #enddocregion route-no-id
// #docregion route-bad-id
describe('when navigate to non-existant hero id', () => {
describe('when navigate to non-existent hero id', () => {
beforeEach( async(() => {
activatedRoute.testParamMap = { id: 99999 };
createComponent();

View File

@ -2,7 +2,7 @@
<div *ngIf="phone">
<div class="phone-images">
<img [src]="img" class="phone"
[ngClass]="{selected: img === mainImageUrl}"
[ngClass]="{'selected': img === mainImageUrl}"
*ngFor="let img of phone.images" />
</div>

View File

@ -2,7 +2,7 @@
<div *ngIf="phone">
<div class="phone-images">
<img [src]="img" class="phone"
[ngClass]="{selected: img === mainImageUrl}"
[ngClass]="{'selected': img === mainImageUrl}"
*ngFor="let img of phone.images" />
</div>

View File

@ -1000,7 +1000,7 @@ For more information on pipes, see [Pipes](guide/pipes).
## Modules/controllers/components
In both AngularJS and Angular, Angular modules help you organize your application into cohesive blocks of functionality.
In both AngularJS and Angular, modules help you organize your application into cohesive blocks of functionality.
In AngularJS, you write the code that provides the model and the methods for the view in a **controller**.
In Angular, you build a **component**.
@ -1080,18 +1080,18 @@ The Angular code is shown using TypeScript.
<td>
### Angular modules
### NgModules
<code-example hideCopy path="ajs-quick-reference/src/app/app.module.1.ts" linenums="false">
</code-example>
Angular modules, defined with the `NgModule` decorator, serve the same purpose:
NgModules, defined with the `NgModule` decorator, serve the same purpose:
* `imports`: specifies the list of other modules that this module depends upon
* `declaration`: keeps track of your components, pipes, and directives.
For more information on modules, see [Angular Modules (NgModule)](guide/ngmodule).
For more information on modules, see [NgModules](guide/ngmodule).
</td>
</tr>

View File

@ -475,7 +475,7 @@ You'll need separate TypeScript configuration files such as these:
<div class="callout is-helpful">
<header>
@Types and node modules
`@types` and node modules
</header>
In the file structure of _this particular sample project_,
@ -568,7 +568,7 @@ Run the following command to generate the map.
</code-example>
The `source-map-explorer` analyzes the source map generated with the bundle and draws a map of all dependencies,
showing exactly which application and Angular modules and classes are included in the bundle.
showing exactly which application and NgModules and classes are included in the bundle.
Here's the map for _Tour of Heroes_.

View File

@ -31,21 +31,21 @@ You'll learn the details in the pages that follow. For now, focus on the big pic
<img src="generated/images/guide/architecture/module.png" alt="Component" class="left">
Angular apps are modular and Angular has its own modularity system called _Angular modules_ or _NgModules_.
Angular apps are modular and Angular has its own modularity system called _NgModules_.
_Angular modules_ are a big deal.
This page introduces modules; the [Angular modules](guide/ngmodule) page covers them in depth.
NgModules are a big deal.
This page introduces modules; the [NgModules](guide/ngmodule) page covers them in depth.
<br class="clear">
Every Angular app has at least one Angular module class, [the _root module_](guide/bootstrapping "AppModule: the root module"),
Every Angular app has at least one NgModule class, [the _root module_](guide/bootstrapping "Bootstrapping"),
conventionally named `AppModule`.
While the _root module_ may be the only module in a small application, most apps have many more
_feature modules_, each a cohesive block of code dedicated to an application domain,
a workflow, or a closely related set of capabilities.
An Angular module, whether a _root_ or _feature_, is a class with an `@NgModule` decorator.
An NgModule, whether a _root_ or _feature_, is a class with an `@NgModule` decorator.
<div class="l-sub-section">
@ -87,12 +87,12 @@ During development you're likely to bootstrap the `AppModule` in a `main.ts` fil
<code-example path="architecture/src/main.ts" title="src/main.ts" linenums="false"></code-example>
### Angular modules vs. JavaScript modules
### NgModules vs. JavaScript modules
The Angular module &mdash; a class decorated with `@NgModule` &mdash; is a fundamental feature of Angular.
The NgModule &mdash; a class decorated with `@NgModule` &mdash; is a fundamental feature of Angular.
JavaScript also has its own module system for managing collections of JavaScript objects.
It's completely different and unrelated to the Angular module system.
It's completely different and unrelated to the NgModule system.
In JavaScript each _file_ is a module and all objects defined in the file belong to that module.
The module declares some objects to be public by marking them with the `export` key word.
@ -124,7 +124,7 @@ For example, import Angular's `Component` decorator from the `@angular/core` lib
<code-example path="architecture/src/app/app.component.ts" region="import" linenums="false"></code-example>
You also import Angular _modules_ from Angular _libraries_ using JavaScript import statements:
You also import NgModules_ from Angular _libraries_ using JavaScript import statements:
<code-example path="architecture/src/app/mini-app.ts" region="import-browser-module" linenums="false"></code-example>
@ -139,7 +139,7 @@ Hang in there. The confusion yields to clarity with time and experience.
<div class="l-sub-section">
Learn more from the [Angular modules](guide/ngmodule) page.
Learn more from the [NgModules](guide/ngmodule) page.
</div>

View File

@ -1,8 +1,8 @@
# Bootstrapping
An Angular module class describes how the application parts fit together.
Every application has at least one Angular module, the _root_ module
that you [bootstrap](guide/bootstrapping#main) to launch the application.
An NgModule class describes how the application parts fit together.
Every application has at least one NgModule, the _root_ module
that you [bootstrap](guide/appmodule#main) to launch the application.
You can call it anything you want. The conventional name is `AppModule`.
The [setup](guide/setup) instructions produce a new project with the following minimal `AppModule`.
@ -17,15 +17,15 @@ You'll evolve this module as your application grows.
After the `import` statements, you come to a class adorned with the
**`@NgModule`** [_decorator_](guide/glossary#decorator '"Decorator" explained').
The `@NgModule` decorator identifies `AppModule` as an Angular module class (also called an `NgModule` class).
The `@NgModule` decorator identifies `AppModule` as an `NgModule` class.
`@NgModule` takes a _metadata_ object that tells Angular how to compile and launch the application.
* **_imports_** &mdash; the `BrowserModule` that this and every application needs to run in a browser.
* **_declarations_** &mdash; the application's lone component, which is also ...
* **_bootstrap_** &mdash; the _root_ component that Angular creates and inserts into the `index.html` host web page.
The [Angular Modules (NgModule)](guide/ngmodule) guide dives deeply into the details of Angular modules.
All you need to know at the moment is a few basics about these three properties.
The [NgModules](guide/ngmodule) guide dives deeply into the details of NgModules.
All you need to know at the moment is a few basics about these three properties.
{@a imports}
@ -33,8 +33,8 @@ All you need to know at the moment is a few basics about these three properties.
### The _imports_ array
Angular modules are a way to consolidate features that belong together into discrete units.
Many features of Angular itself are organized as Angular modules.
NgModules are a way to consolidate features that belong together into discrete units.
Many features of Angular itself are organized as NgModules.
HTTP services are in the `HttpModule`. The router is in the `RouterModule`.
Eventually you may create a feature module.
@ -61,7 +61,7 @@ Other guide and cookbook pages will tell you when you need to add additional mod
The `import` statements at the top of the file and the Angular module's `imports` array
The `import` statements at the top of the file and the NgModule's `imports` array
are unrelated and have completely different jobs.
The _JavaScript_ `import` statements give you access to symbols _exported_ by other files
@ -70,8 +70,8 @@ You add `import` statements to almost every application file.
They have nothing to do with Angular and Angular knows nothing about them.
The _module's_ `imports` array appears _exclusively_ in the `@NgModule` metadata object.
It tells Angular about specific _other_ Angular modules &mdash; all of them classes decorated with `@NgModule` &mdash;
that the application needs to function properly.
It tells Angular about specific _other_ NgModules&mdash;all of them classes decorated
with `@NgModule`&mdash;that the application needs to function properly.
</div>
@ -178,11 +178,11 @@ This file is very stable. Once you've set it up, you may never change it again.
## More about Angular Modules
## More about NgModules
Your initial app has only a single module, the _root_ module.
As your app grows, you'll consider subdividing it into multiple "feature" modules,
some of which can be loaded later ("lazy loaded") if and when the user chooses
to visit those features.
When you're ready to explore these possibilities, visit the [Angular Modules (NgModule)](guide/ngmodule) guide.
When you're ready to explore these possibilities, visit the [NgModules](guide/ngmodule) guide.

View File

@ -199,7 +199,7 @@ The new "angular-in-memory-web-api" has new features.
## "Style Guide" with _NgModules_ (2016-09-27)
[StyleGuide](guide/styleguide) explains recommended conventions for Angular modules (NgModule).
[StyleGuide](guide/styleguide) explains recommended conventions for NgModules.
Barrels now are far less useful and have been removed from the style guide;
they remain valuable but are not a matter of Angular style.
Also relaxed the rule that discouraged use of the `@Component.host` property.

View File

@ -136,7 +136,7 @@ is available to <code>declarations</code> of this module.</p>
<td><p>Conditionally swaps the contents of the div by selecting one of the embedded templates based on the current value of <code>conditionExpression</code>.</p>
</td>
</tr><tr>
<td><code>&lt;div&nbsp;<b>[ngClass]</b>="{active:&nbsp;isActive,&nbsp;disabled:&nbsp;isDisabled}"&gt;</code></td>
<td><code>&lt;div&nbsp;<b>[ngClass]</b>="{'active':&nbsp;isActive,&nbsp;'disabled':&nbsp;isDisabled}"&gt;</code></td>
<td><p>Binds the presence of CSS classes on the element to the truthiness of the associated map values. The right-hand expression should return {class-name: true/false} map.</p>
</td>
</tr>

View File

@ -244,7 +244,7 @@ You can limit the scope of an injected service to a *branch* of the application
by providing that service *at the sub-root component for that branch*.
This example shows how similar providing a service to a sub-root component is
to providing a service in the root `AppComponent`. The syntax is the same.
Here, the `HeroService` is availble to the `HeroesBaseComponent` because it is in the `providers` array:
Here, the `HeroService` is available to the `HeroesBaseComponent` because it is in the `providers` array:
<code-example path="dependency-injection-in-action/src/app/sorted-heroes.component.ts" region="injection" title="src/app/sorted-heroes.component.ts (HeroesBaseComponent excerpt)">

View File

@ -320,7 +320,7 @@ Read more about `ngIf` and `*` in the [ngIf section](guide/template-syntax#ngIf)
The template expression inside the double quotes,
`*ngIf="heros.length > 3"`, looks and behaves much like TypeScript.
`*ngIf="heroes.length > 3"`, looks and behaves much like TypeScript.
When the component's list of heroes has more than three items, Angular adds the paragraph
to the DOM and the message appears. If there are three or fewer items, Angular omits the
paragraph, so no message appears. For more information,

View File

@ -40,9 +40,9 @@ The flat folder approach allows us to shuffle the apparent navigation structure
The doc generation process consumes the markdown files in the `content/guide` directory and produces JSON files in the `src/generated/docs/guide` directory, which is also flat. Those JSON files contain a combination of document metadata and HTML content.
The reader request a page by its Page URL. The doc viewer fetches the corresponding JSON file, interprets it, and renders it as fully-formed HTML page.
The reader requests a page by its Page URL. The doc viewer fetches the corresponding JSON file, interprets it, and renders it as fully-formed HTML page.
Page URLs mirror the `content` file structure. A guide page URL is in the form `guide/{page-name}`. _This_ guide page is located at `context/guide/docs-style-guide.md` and its URL is `guide/docs-style-guide`.
Page URLs mirror the `content` file structure. The URL for the page of a guide is in the form `guide/{page-name}`. The page for _this_ "Authors Style Guide" is located at `content/guide/docs-style-guide.md` and its URL is `guide/docs-style-guide`.
<div class="l-sub-section">
@ -64,7 +64,7 @@ While documentation guide pages ultimately render as HTML, almost all of them ar
Markdown is easier to read and to edit than HTML. Many editors (including Visual Studio Code) can render markdown as you type it.
From time to time you'll have to step away from markdown and write a portion of the document in HTML. markdown allows you to mix HTML and markdown in the same document.
From time to time you'll have to step away from markdown and write a portion of the document in HTML. Markdown allows you to mix HTML and markdown in the same document.
Standard markdown processors don't allow you to put markdown _within_ HTML tags. But the Angular documentation markdown processor **supports markdown within HTML**, as long as you follow one rule:
@ -77,14 +77,14 @@ Standard markdown processors don't allow you to put markdown _within_ HTML tags.
```html
<div class="alert is-critical">
**Always** follow every opening and closing opening HTML tag with _a blank line_.
**Always** follow every opening and closing HTML tag with _a blank line_.
</div>
```
<div class="l-sub-section">
It is customary but not required to precede the _closing HTML_ tag with a blank tag as well.
It is customary but not required to _precede_ the _closing HTML_ tag with a blank line as well.
</div>
@ -101,7 +101,7 @@ Begin the title with the markdown `#` character. Alternatively, you can write th
**Only one title (`<h1>`) per document!**
Title text should be in "Title Case", which means that you use capital letters to start the first works and all _principal_. Use lower case letters for _secondary words such as "in", "of", and "the".
Title text should be in "Title Case", which means that you use capital letters to start the first words and all _principal_ words. Use lower case letters for _secondary words such as "in", "of", and "the".
```html
# The Meat of the Matter
@ -113,7 +113,7 @@ Title text should be in "Title Case", which means that you use capital letters t
A typical document is divided into sections.
All section heading text should be in "Sentence Case", which means the first word is capitalized and all other words are lower case.
All section heading text should be in "Sentence case", which means the first word is capitalized and all other words are lower case.
**Always follow the section heading with at least one blank line.**
@ -313,7 +313,7 @@ In all other cases, code snippets should be generated automatically from tested
One of the documentation design goals is that guide page code snippets should be examples of real, working code.
We meet this goal by displaying code snippets that are derived directly from standalone code samples, written specifically for these guide page.
We meet this goal by displaying code snippets that are derived directly from standalone code samples, written specifically for these guide pages.
The author of a guide page is responsible for the code sample that supports that page.
The author must also write end-to-end tests for the sample.
@ -343,10 +343,11 @@ Here's the brief markup that produced that lengthy snippet:
```html
<code-example
path="docs-style-guide/src/app/app.module.ts"
title="src/app/app.module.ts"></code-example>
title="src/app/app.module.ts">
</code-example>
```
You identified the snippet's source file by setting the `path` attribute to sample folder's location_within `content/examples`.
You identified the snippet's source file by setting the `path` attribute to sample folder's location _within_ `content/examples`.
In this example, that path is `docs-style-guide/src/app/app.module.ts`.
You added a header to tell the reader where to find the file by setting the `title` attribute.
@ -369,7 +370,7 @@ You control the _code-example_ output by setting one or more of its attributes:
* `region`- displays the source file fragment with that region name; regions are identified by _docregion_ markup in the source file, as explained [below](#region "Displaying a code fragment").
* `linenums`- value may be `true`, `false`, or a `number`. When not specified, line numbers are automatically displayed when there are 10 or more lines of code. The rarely used `number` option starts line numbering at the given value. `linenums=4` sets the starting line number to 4.
* `linenums`- value may be `true`, `false`, or a `number`. When not specified, line numbers are automatically displayed when there are greater than 10 lines of code. The rarely used `number` option starts line numbering at the given value. `linenums=4` sets the starting line number to 4.
* `class`- code snippets can be styled with the CSS classes `no-box`, `code-shell`, and `avoid`.
@ -385,7 +386,8 @@ Often you want to focus on a fragment of code within a sample code file. In this
<code-example
path="docs-style-guide/src/app/app.module.ts"
region="class"></code-example>
region="class">
</code-example>
First you surround that fragment in the source file with a named _docregion_ as described [below](#source-code-markup).
Then you reference that _docregion_ in the `region` attribute of the `<code-example>` like this
@ -394,12 +396,13 @@ Then you reference that _docregion_ in the `region` attribute of the `<code-exam
```html
<code-example
path="docs-style-guide/src/app/app.module.ts"
region="class"></code-example>
region="class">
</code-example>
```
A couple of observations:
1. The `region` value, `"class"`, is the name of the `#docregion` in the source file. Confirm that by looking at `content/examples/ocs-style-guide/src/app/app.module.ts`
1. The `region` value, `"class"`, is the name of the `#docregion` in the source file. Confirm that by looking at `content/examples/docs-style-guide/src/app/app.module.ts`
1. Omitting the `title` is fine when the source of the fragment is obvious. We just said that this is a fragment of the `app.module.ts` file which was displayed immediately above, in full, with a header.
There's no need to repeat the header.
@ -431,21 +434,20 @@ Here's the markup for an "avoid" example in the
{@a code-tabs}
### Code Tabs
Code tabs display code much like Code examples do. The added advantage is that they can display mutiple code samples within a tabbed interface. Each tab is displayed using Code-pane.
Code tabs display code much like _code examples_ do. The added advantage is that they can display mutiple code samples within a tabbed interface. Each tab is displayed using _code pane_.
#### Code-tabs attributes
* `linenums`: The value can be `true`, `false` or a number indicating the starting line number. If not specified, line numbers are are enabled only when code for a tab pane has 10 or more lines.
* `linenums`: The value can be `true`, `false` or a number indicating the starting line number. If not specified, line numbers are enabled only when code for a tab pane has greater than 10 lines of code.
#### Code-pane attributes
* `path` - a file in the content/examples folder
* `title` - seen in the header of a tab
* `linenums` - overrides the `linenums` property at the `code-tabs` level for this particular pane. The value can be `true`, `false` or a number indicating the starting line number. If not specified, line numbers are are enabled only when there are 10 or more lines.
* `linenums` - overrides the `linenums` property at the `code-tabs` level for this particular pane. The value can be `true`, `false` or a number indicating the starting line number. If not specified, line numbers are enabled only when the number of lines of code are greater than 10.
The next example displays multiple code tabs, each with its own title.
Line numbers are explicitly disabled for all panes at the `<code-tabs>` level.
The second pane adds line numbers back for itself.
It demonstrates control over display of line numbers at both the `<code-tabs>` and `<code-pane>` levels.
<code-tabs linenums="false">
<code-pane
@ -470,6 +472,9 @@ The second pane adds line numbers back for itself.
Here's the markup for that example.
Note how the `linenums` attribute in the `<code-tabs>` explicitly disables numbering for all panes.
The `linenums` attribute in the second pane restores line numbering for _itself only_.
```html
<code-tabs linenums="false">
<code-pane
@ -763,7 +768,7 @@ To link to a plunker defined by a named `plnkr.json` file, set the `plnkr` attri
<h3 class="no-toc">Live Example without download</h3>
To skip the download link, add the `noDownload` attribute
To skip the download link, add the `noDownload` attribute.
<live-example noDownload>Just the plunker</live-example>
@ -781,7 +786,7 @@ To skip the live plunker link and only link to the download, add the `downloadOn
<live-example downloadOnly>Download only</live-example>
```
<h3 class="no-toc"Embedded live example></h3>
<h3 class="no-toc">Embedded live example</h3>
By default, a live example link opens a plunker in a separate browser tab.
You can embed the plunker within the guide page itself by adding the `embedded` attribute.

View File

@ -64,7 +64,7 @@ This gives you a reference to the Angular `NgModel` directive
associated with this control that you can use _in the template_
to check for control states such as `valid` and `dirty`.
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
* The `*ngIf` on the `<div>` element reveals a set of nested message `divs`
but only if there are `name` errors and
the control is either `dirty` or `touched`.
@ -168,10 +168,8 @@ on custom validation directives.
* Binding to the new `formErrors.name` property is sufficient to display all name validation error messages.
{@a component-class}
### Component class
The original component code for Template 1 stayed the same; however,
Template 2 requires some changes in the component. This section covers the code
@ -323,7 +321,7 @@ This allows you to do the following:
* Add, change, and remove validation functions on the fly.
* Manipulate the control model dynamically from within the component.
* [Test](guide/form-validation#testing) validation and control logic with isolated unit tests.
* [Test](guide/form-validation#testing-considerations) validation and control logic with isolated unit tests.
The following sample re-writes the hero form in Reactive Forms style.
@ -388,7 +386,7 @@ but rather for css styling and accessibility.
<div class="l-sub-section">
Currently, Reactive Forms doesn't add the `required` or `aria-required`
Currently, Reactive Forms doesn't add the `required` or `aria-required`
HTML validation attribute to the DOM element
when the control has the `required` validator function.
@ -457,12 +455,12 @@ to set error messages for the new control model.
## Built-in validators
Angular forms include a number of built-in validator functions, which are functions
that help you check common user input in forms. In addition to the built-in
validators covered here of `minlength`, `maxlength`,
and `required`, there are others such as `email` and `pattern`
for Reactive Forms.
For a full list of built-in validators,
Angular forms include a number of built-in validator functions, which are functions
that help you check common user input in forms. In addition to the built-in
validators covered here of `minlength`, `maxlength`,
and `required`, there are others such as `email` and `pattern`
for Reactive Forms.
For a full list of built-in validators,
see the [Validators](api/forms/Validators) API reference.
@ -488,7 +486,7 @@ Learn more about `FormBuilder` in the [Introduction to FormBuilder](guide/reacti
#### Committing hero value changes
In two-way data binding, the user's changes flow automatically from the controls back to the data model properties.
A Reactive Forms component should not use data binding to
A Reactive Forms component should not use data binding to
automatically update data model properties.
The developer decides _when and how_ to update the data model from control values.

View File

@ -214,10 +214,10 @@ There are three changes:
1. You import `FormsModule` and the new `HeroFormComponent`.
1. You add the `FormsModule` to the list of `imports` defined in the `ngModule` decorator. This gives the application
1. You add the `FormsModule` to the list of `imports` defined in the `@NgModule` decorator. This gives the application
access to all of the template-driven forms features, including `ngModel`.
1. You add the `HeroFormComponent` to the list of `declarations` defined in the `ngModule` decorator. This makes
1. You add the `HeroFormComponent` to the list of `declarations` defined in the `@NgModule` decorator. This makes
the `HeroFormComponent` component visible throughout this module.

View File

@ -25,15 +25,8 @@ to a module factory, meaning you don't need to include the Angular compiler in y
Ahead-of-time compiled applications also benefit from decreased load time and increased performance.
## Angular module
Helps you organize an application into cohesive blocks of functionality.
An Angular module identifies the components, directives, and pipes that the application uses along with the list of external Angular modules that the application needs, such as `FormsModule`.
Every Angular application has an application root-module class. By convention, the class is
called `AppModule` and resides in a file named `app.module.ts`.
For details and examples, see the [Angular Modules (NgModule)](guide/ngmodule) page.
</div>
## Annotation
@ -115,7 +108,7 @@ The Angular [scoped packages](guide/glossary#scoped-package) each have a barrel
You can often achieve the same result using [Angular modules](guide/glossary#angular-module) instead.
You can often achieve the same result using [NgModules](guide/glossary#ngmodule) instead.
</div>
@ -132,7 +125,11 @@ between a "token"&mdash;also referred to as a "key"&mdash;and a dependency [prov
## Bootstrap
You launch an Angular application by "bootstrapping" it using the application root Angular module (`AppModule`).
<div class="l-sub-section">
You launch an Angular application by "bootstrapping" it using the application root NgModule (`AppModule`).
Bootstrapping identifies an application's top level "root" [component](guide/glossary#component),
which is the first component that is loaded for the application.
For more information, see the [Setup](guide/setup) page.
@ -346,13 +343,13 @@ elements and their children.
The [official JavaScript language specification](https://en.wikipedia.org/wiki/ECMAScript).
The latest approved version of JavaScript is
[ECMAScript 2016](http://www.ecma-international.org/ecma-262/7.0/)
(also known as "ES2016" or "ES7"). Many Angular developers write their applications
in ES7 or a dialect that strives to be
[ECMAScript 2017](http://www.ecma-international.org/ecma-262/8.0/)
(also known as "ES2017" or "ES8"). Many Angular developers write their applications
in ES8 or a dialect that strives to be
compatible with it, such as [TypeScript](guide/glossary#typescript).
Most modern browsers only support the much older "ECMAScript 5" (also known as "ES5") standard.
Applications written in ES2016, ES2015, or one of their dialects must be [transpiled](guide/glossary#transpile)
Applications written in ES2017, ES2016, ES2015, or one of their dialects must be [transpiled](guide/glossary#transpile)
to ES5 JavaScript.
Angular developers can write in ES5 directly.
@ -475,8 +472,8 @@ Read more in the [Lifecycle Hooks](guide/lifecycle-hooks) page.
Angular has the following types of modules:
* [Angular modules](guide/glossary#angular-module).
For details and examples, see the [Angular Modules](guide/ngmodule) page.
* [NgModules](guide/glossary#ngmodule).
For details and examples, see the [NgModules](guide/ngmodule) page.
* ES2015 modules, as described in this section.
@ -493,7 +490,7 @@ In general, you assemble an application from many modules, both the ones you wri
A module *exports* something of value in that code, typically one thing such as a class;
a module that needs that class *imports* it.
The structure of Angular modules and the import/export syntax
The structure of NgModules and the import/export syntax
is based on the [ES2015 module standard](http://www.2ality.com/2014/09/es6-modules-final.html).
An application that adheres to this standard requires a module loader to
@ -511,6 +508,24 @@ You rarely access Angular feature modules directly. You usually import them from
{@a N}
## NgModule
<div class="l-sub-section">
Helps you organize an application into cohesive blocks of functionality.
An NgModule identifies the components, directives, and pipes that the application uses along with the list of external NgModules that the application needs, such as `FormsModule`.
Every Angular application has an application root-module class. By convention, the class is
called `AppModule` and resides in a file named `app.module.ts`.
For details and examples, see [NgModules](guide/ngmodule).
</div>
{@a O}
## Observable
@ -614,7 +629,9 @@ For more information, see the [Routing & Navigation](guide/router) page.
## Router module
A separate [Angular module](guide/glossary#angular-module) that provides the necessary service providers and directives for navigating through application views.
<div class="l-sub-section">
A separate [NgModule](guide/glossary#ngmodule) that provides the necessary service providers and directives for navigating through application views.
For more information, see the [Routing & Navigation](guide/router) page.
@ -633,7 +650,7 @@ For more information, see the [Routing & Navigation](guide/router) page.
A way to group related *npm* packages.
Read more at the [npm-scope](https://docs.npmjs.com/misc/scope) page.
Angular modules are delivered within *scoped packages* such as `@angular/core`,
NgModules are delivered within *scoped packages* such as `@angular/core`,
`@angular/common`, `@angular/platform-browser-dynamic`, `@angular/http`, and `@angular/router`.
Import a scoped package the same way that you import a normal package.
@ -795,4 +812,4 @@ asynchronous events by checking for data changes and updating
the information it displays via [data bindings](guide/glossary#data-binding).
Learn more about zones in this
[Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U).
[Brian Ford video](https://www.youtube.com/watch?v=3IqtmUscE_U).

View File

@ -1327,7 +1327,7 @@ Here's an _NgModule_ class with imports, exports, and declarations.
Of course you use _JavaScript_ modules to write _Angular_ modules as seen in the complete `contact.module.ts` file:
Of course you use _JavaScript_ modules to write NgModules as seen in the complete `contact.module.ts` file:
<code-example path="ngmodule/src/app/contact/contact.module.2.ts" title="src/app/contact/contact.module.ts" linenums="false">

View File

@ -140,7 +140,7 @@ This is the _root component_ and it is named `app-root`.
You can find it in `./src/app/app.component.ts`.
Open the component file and change the `title` property from _app works!_ to _My First Angular App_:
Open the component file and change the `title` property from _Welcome to app!!_ to _Welcome to My First Angular App!!_:
<code-example path="cli-quickstart/src/app/app.component.ts" region="title" title="src/app/app.component.ts" linenums="false"></code-example>
@ -331,7 +331,7 @@ Any files outside of this folder are meant to support building your app.
The main HTML page that is served when someone visits your site.
Most of the time you'll never need to edit it.
The CLI automatically adds all `js` and `css` files when building your app so you
never need to add any `&lt;script&gt;` or `&lt;link&gt;` tags here manually.
never need to add any `<script>` or `<link>` tags here manually.
</td>
</tr>
@ -464,7 +464,7 @@ These files go in the root folder next to `src/`.
</td>
<td>
Inside `e2e/` live the End-to-End tests.
Inside `e2e/` live the end-to-end tests.
They shouldn't be inside `src/` because e2e tests are really a separate app that
just so happens to test your main app.
That's also why they have their own `tsconfig.e2e.json`.
@ -493,7 +493,7 @@ These files go in the root folder next to `src/`.
Configuration for Angular CLI.
In this file you can set several defaults and also configure what files are included
when your project is build.
when your project is built.
Check out the official documentation if you want to know more.
</td>

View File

@ -614,7 +614,7 @@ If you do not bind the value, the select shows the first option from the data mo
The component _class_ defines control properties without regard for their representation in the template.
You define the `state`, `power`, and `sidekick` controls the same way you defined the `name` control.
You tie these controls to the template HTML elements in the same way,
specifiying the `FormControl` name with the `formControlName` directive.
specifying the `FormControl` name with the `formControlName` directive.
See the API reference for more information about
[radio buttons](api/forms/RadioControlValueAccessor "API: RadioControlValueAccessor"),
@ -1220,7 +1220,7 @@ Place a button on the form so the user can add a new _secret lair_ and wire it t
Be sure to **add the `type="button"` attribute**.
In fact, you should always specify a button's `type`.
Without an explict type, the button type defaults to "submit".
Without an explicit type, the button type defaults to "submit".
When you later add a _form submit_ action, every "submit" button triggers the submit action which
might do something like save the current changes.
You do not want to save changes when the user clicks the _Add a Secret Lair_ button.

View File

@ -347,7 +347,7 @@ Here are the key `Router` terms and their meanings:
</td>
<td>
A separate Angular module that provides the necessary service providers
A separate NgModule that provides the necessary service providers
and directives for navigating through application views.
</td>
@ -3794,7 +3794,7 @@ Take the final step and detach the admin feature set from the main application.
The root `AppModule` must neither load nor reference the `AdminModule` or its files.
In `app.module.ts`, remove the `AdminModule` import statement from the top of the file
and remove the `AdminModule` from the Angular module's `imports` array.
and remove the `AdminModule` from the NgModule's `imports` array.
{@a can-load-guard}

View File

@ -199,7 +199,7 @@ If you do, this page can help you understand their purpose.
A list of files that you can delete if you want to purge your setup of the
original QuickStart Seed testing and git maintainence artifacts.
original QuickStart Seed testing and git maintenance artifacts.
See instructions in the optional
[_Deleting non-essential files_](guide/setup#non-essential "Setup: Deleting non-essential files") section.
*Do this only in the beginning to avoid accidentally deleting your own tests and git setup!*

View File

@ -2129,12 +2129,12 @@ discourage the `I` prefix.
<a href="#toc">Back to top</a>
## Application structure and Angular modules
## Application structure and NgModules
Have a near-term view of implementation and a long-term vision. Start small but keep in mind where the app is heading down the road.
All of the app's code goes in a folder named `src`.
All feature areas are in their own folder, with their own Angular module.
All feature areas are in their own folder, with their own NgModule.
All content is one asset per file. Each component, service, and pipe is in its own file.
All third party vendor scripts are stored in another folder and not in the `src` folder.
@ -2779,7 +2779,7 @@ and more difficult in a flat structure.
**Do** create an Angular module for each feature area.
**Do** create an NgModule for each feature area.
</div>
@ -2790,7 +2790,7 @@ and more difficult in a flat structure.
**Why?** Angular modules make it easy to lazy load routable features.
**Why?** NgModules make it easy to lazy load routable features.
</div>
@ -2801,7 +2801,7 @@ and more difficult in a flat structure.
**Why?** Angular modules make it easier to isolate, test, and re-use features.
**Why?** NgModules make it easier to isolate, test, and re-use features.
</div>
@ -2827,7 +2827,7 @@ and more difficult in a flat structure.
**Do** create an Angular module in the app's root folder,
**Do** create an NgModule in the app's root folder,
for example, in `/src/app`.
@ -2839,7 +2839,7 @@ for example, in `/src/app`.
**Why?** Every app requires at least one root Angular module.
**Why?** Every app requires at least one root NgModule.
</div>
@ -2888,7 +2888,7 @@ for example, in `/src/app`.
**Do** create an Angular module for all distinct features in an application;
**Do** create an NgModule for all distinct features in an application;
for example, a `Heroes` feature.
@ -3127,7 +3127,7 @@ that may need features from another common module; for example,
**Why?** A lazy loaded feature module that imports that shared module will make its own copy of the service and likely have undesireable results.
**Why?** A lazy loaded feature module that imports that shared module will make its own copy of the service and likely have undesirable results.
</div>
@ -3377,7 +3377,7 @@ Yet they're too big and messy to leave loose in the root folder.
**Why?** A lazily loaded feature module that directly imports the `CoreModule` will make its own copy of services and likely have undesireable results.
**Why?** A lazily loaded feature module that directly imports the `CoreModule` will make its own copy of services and likely have undesirable results.
</div>

View File

@ -12,8 +12,8 @@ component class instance (the *component*) and its user-facing template.
You may be familiar with the component/template duality from your experience with model-view-controller (MVC) or model-view-viewmodel (MVVM).
In Angular, the component plays the part of the controller/viewmodel, and the template represents the view.
This page is a comprehensive technical reference to the Angular template language.
It explains basic principles of the template language and describes most of the syntax that you'll encounter elsewhere in the documentation.
This page is a comprehensive technical reference to the Angular template language.
It explains basic principles of the template language and describes most of the syntax that you'll encounter elsewhere in the documentation.
Many code snippets illustrate the points and concepts, all of them available
in the <live-example title="Template Syntax Live Code"></live-example>.
@ -1155,7 +1155,7 @@ other HTML elements, attributes, properties, and components.
They are usually applied to elements as if they were HTML attributes, hence the name.
Many details are covered in the [_Attribute Directives_](guide/attribute-directives) guide.
Many Angular modules such as the [`RouterModule`](guide/router "Routing and Navigation")
Many NgMdules such as the [`RouterModule`](guide/router "Routing and Navigation")
and the [`FormsModule`](guide/forms "Forms") define their own attribute directives.
This section is an introduction to the most commonly used attribute directives:
@ -1260,7 +1260,7 @@ Two-way data binding with the `NgModel` directive makes that easy. Here's an exa
#### _FormsModule_ is required to use _ngModel_
Before using the `ngModel` directive in a two-way data binding,
you must import the `FormsModule` and add it to the Angular module's `imports` list.
you must import the `FormsModule` and add it to the NgModule's `imports` list.
Learn more about the `FormsModule` and `ngModel` in the
[Forms](guide/forms#ngModel) guide.
@ -1371,7 +1371,7 @@ _This_ section is an introduction to the common structural directives:
### NgIf
You can add or remove an element from the DOM by applying an `NgIf` directive to
that element (called the _host elment_).
that element (called the _host element_).
Bind the directive to a condition expression like `isActive` in this example.
<code-example path="template-syntax/src/app/app.component.html" region="NgIf-1" title="src/app/app.component.html" linenums="false">
@ -1947,12 +1947,12 @@ As of Typescript 2.0, you can enforce [strict null checking](http://www.typescri
In this mode, typed variables disallow null and undefined by default. The type checker throws an error if you leave a variable unassigned or try to assign null or undefined to a variable whose type disallows null and undefined.
The type checker also throws an error if it can't determine whether a variable will be null or undefined at runtime.
You may know that can't happen but the type checker doesn't know.
The type checker also throws an error if it can't determine whether a variable will be null or undefined at runtime.
You may know that can't happen but the type checker doesn't know.
You tell the type checker that it can't happen by applying the post-fix
[_non-null assertion operator (!)_]((http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator "Non-null assertion operator").
[_non-null assertion operator (!)_](http://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator "Non-null assertion operator").
The _Angular_ **non-null assertion operator (`!`)** serves the same purpose in an Angular template.
The _Angular_ **non-null assertion operator (`!`)** serves the same purpose in an Angular template.
For example, after you use [*ngIf](guide/template-syntax#ngIf) to check that `hero` is defined, you can assert that
`hero` properties are also defined.

View File

@ -427,7 +427,7 @@ and re-attach it to a dynamically-constructed Angular test module
tailored specifically for this battery of tests.
The `configureTestingModule` method takes an `@NgModule`-like metadata object.
The metadata object can have most of the properties of a normal [Angular module](guide/ngmodule).
The metadata object can have most of the properties of a normal [NgModule](guide/ngmodule).
_This metadata object_ simply declares the component to test, `BannerComponent`.
The metadata lack `imports` because (a) the default testing module configuration already has what `BannerComponent` needs

View File

@ -431,7 +431,7 @@ by calling the `@Inject()` decorator with the injection token.
In the following example, the token is the string `'heroName'`.
The other JavaScript dialects add a `parameters` array to the class constructor function.
Each item constrains a new instance of `Inject`:
Each item contains a new instance of `Inject`:
* _Plain ES6_&mdash;each item is a new instance of `Inject(token)` in a sub-array.
* _ES5_&mdash;simply list the string tokens.

View File

@ -62,7 +62,7 @@ There are a few rules in particular that will make it much easier to do
* The [Folders-by-Feature Structure](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#folders-by-feature-structure)
and [Modularity](https://github.com/johnpapa/angular-styleguide/blob/master/a1/README.md#modularity)
rules define similar principles on a higher level of abstraction: Different parts of the
application should reside in different directories and Angular modules.
application should reside in different directories and NgModules.
When an application is laid out feature per feature in this way, it can also be
migrated one feature at a time. For applications that don't already look like
@ -382,12 +382,12 @@ that describes Angular assets in metadata. The differences blossom from there.
In a hybrid application you run both versions of Angular at the same time.
That means that you need at least one module each from both AngularJS and Angular.
You will import `UpgradeModule` inside the Angular module, and then use it for
You will import `UpgradeModule` inside the NgModule, and then use it for
bootstrapping the AngularJS module.
<div class="l-sub-section">
Learn more about Angular modules at the [NgModule guide](guide/ngmodule).
Read more about [NgModules](guide/ngmodule).
</div>
@ -485,7 +485,7 @@ Because `HeroDetailComponent` is an Angular component, you must also add it to t
And because this component is being used from the AngularJS module, and is an entry point into
the Angular application, you must add it to the `entryComponents` for the
Angular module.
NgModule.
<code-example path="upgrade-module/src/app/downgrade-static/app.module.ts" region="ngmodule" title="app.module.ts">
</code-example>

View File

@ -243,7 +243,7 @@ Everything seems fine while you move about _within_ the app.
But you'll see the problem right away if you refresh the browser
or paste a link to an app page (called a "deep link") into the browser address bar.
You'll most likely get a *404 - Page Not Found* response from the serer
You'll most likely get a *404 - Page Not Found* response from the server
for any address other than `/` or `/index.html`.
You have to configure the server to return `index.html` for requests to these "unknown" pages.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -2,7 +2,7 @@
<h1 class="banner-headline no-toc no-anchor">Events</h1>
</header>
<article class="l-content ">
<article>
<p>Where we'll be presenting:</p>
<table class="is-full-width">
<thead>

View File

@ -2,7 +2,7 @@
<h1 class="banner-headline no-toc no-anchor">Features & Benefits</h1>
</header>
<article class="l-content ">
<article>
<div class="flex-center">
<div>
<div class="feature-section">
@ -28,6 +28,7 @@
<p class="text-body">Create desktop-installed apps across Mac, Windows, and Linux using the same Angular methods you've learned for the web plus the ability to access native OS APIs.</p>
</div>
</div>
<hr>
</div>
<div class="feature-section">
@ -52,6 +53,7 @@
<p class="text-body">Angular apps load quickly with the new Component Router, which delivers automatic code-splitting so users only load code required to render the view they request.</p>
</div>
</div>
<hr>
</div>
<div class="feature-section">
@ -76,6 +78,7 @@
<p class="text-body">Get intelligent code completion, instant errors, and other feedback in popular editors and IDEs.</p>
</div>
</div>
<hr>
</div>
<div class="feature-section">

View File

@ -1,5 +1,5 @@
<!--FULL HEADER BLOCK-->
<header class="l-relative">
<header>
<!--BACKGROUND IMAGE-->
<div class="background-sky hero"></div>
@ -24,7 +24,7 @@
</header>
<article class="l-content">
<article>
<div class="home-rows">
<!--Announcement Bar-->

View File

@ -2,7 +2,7 @@
<h1 class="hero-title no-toc">News</h1>
<div class="clear"></div>
</header>
<article class="l-content ">
<artice>
<div class="grid-fluid l-space-bottom-2">
<div class="c12 text-center"><h3 class="text-headline text-uppercase"> Core Team</h3></div>
<div class="clear"></div>

View File

@ -0,0 +1,13 @@
# Contributing to resources.json
## About this list
We maintain a small list of some of the top Angular resources from across the community, stored in `resources.json`. This list is not intended to be comprehensive, but to act as a starting point to connect Angular developers to the rest of the community.
## How do I get listed?
While we can't accept all contributions, qualifying contributions can be submitted via a PR adding yourself to the `resources.json` file. All contributions should be in the appropriate section and must meet the following criteria:
1. Your contribution must be valid, and contain a link to a page talking specifically about using Angular
1. Your contribution should have a clear and concise title and description
1. Your resource should follow our brand guidelines (see our [Presskit](https://angular.io/presskit))
1. Your resource should have significant benefit to Angular developers
1. Your resource should already have traction and praise from Angular developers

View File

@ -227,6 +227,12 @@
"UI Components": {
"order": 4,
"resources": {
"DevExtreme": {
"desc": "50+ UI components including data grid, pivot grid, scheduler, charts, editors, maps and other multi-purpose controls for creating highly responsive web applications for touch devices and traditional desktops.",
"rev": true,
"title": "DevExtreme",
"url": "https://js.devexpress.com/Overview/Angular/"
},
"234237": {
"desc": "UX guidelines, HTML/CSS framework, and Angular components working together to craft exceptional experiences",
"rev": true,
@ -310,7 +316,14 @@
"desc": "Angular UI Components including Grids, Charts, Scheduling and more.",
"rev": true,
"title": "jQWidgets",
"url": "https://www.jqwidgets.com/angular/"
"url": "http://www.jqwidgets.com/angular/"
},
"amexio": {
"desc": "Amexio (Angular MetaMagic EXtensions for Inputs and Outputs) is a rich set of Angular components powered by Bootstrap for Responsive Design. UI Components include Standard Form Components, Data Grids, Tree Grids, Tabs etc. Open Source (Apache 2 License) & Free and backed by MetaMagic Global Inc",
"rev": true,
"title": "Amexio - Angular Extensions",
"url": "http://www.amexio.tech/",
"logo": "http://www.amexio.org/amexio-logo.png"
}
}
}
@ -502,12 +515,6 @@
"title": "Learn Angular (francais)",
"url": "http://www.learn-angular.fr/"
},
"sad200": {
"desc": "Free Angular training delivered by SFEIR in France",
"rev": true,
"title": "SFEIR School (French)",
"url": "https://school.sfeir.com/project/sad-200/"
},
"toddmotto-ultimateangular": {
"desc": "Online courses providing in-depth coverage of the Angular ecosystem, AngularJS, Angular and TypeScript, with functional code samples and a full-featured seed environment. Get a deep understanding of Angular and TypeScript from foundation to functional application, then move on to advanced topics with Todd Motto and collaborators.",
"rev": true,
@ -594,6 +601,12 @@
"rev": true,
"title": "Learn Javascript (Russian)",
"url": "https://learn.javascript.ru/courses/angular"
},
"sa200": {
"desc": "Free Angular training delivered by SFEIR in France",
"rev": true,
"title": "SFEIR School (French)",
"url": "https://school.sfeir.com/project/sa200/"
}
}
}

View File

@ -470,6 +470,8 @@
],
"docVersions": [
{ "title": "v2", "url": "https://v2.angular.io" }
{ "title": "v2", "url": "https://v2.angular.io" },
{ "title": "AngularDart", "url": "https://webdev.dartlang.org/angular" }
]
}

View File

@ -249,7 +249,7 @@ Here's the complete `HeroDetailComponent`.
## Declare _HeroDetailComponent_ in the _AppModule_
Every component must be declared in one&mdash;and only one&mdash;Angular module.
Every component must be declared in one&mdash;and only one&mdash;NgModule.
Open `app.module.ts` in your editor and import the `HeroDetailComponent` so you can refer to it.
@ -276,7 +276,7 @@ This module declares only the two application components, `AppComponent` and `He
Read more about Angular modules in the [NgModules](guide/ngmodule "Angular Modules") guide.
Read more about NgModules in the [NgModules](guide/ngmodule "NgModules") guide.
</div>
@ -449,8 +449,8 @@ Here's what you achieved in this page:
* You created a reusable component.
* You learned how to make a component accept input.
* You learned to declare the required application directives in an Angular module. You
listed the directives in the `NgModule` decorator's `declarations` array.
* You learned to declare the required application directives in an NgModule. You
listed the directives in the `@NgModule` decorator's `declarations` array.
* You learned to bind a parent component to a child component.
Your app should look like this <live-example></live-example>.

View File

@ -30,7 +30,7 @@ You can keep building the Tour of Heroes without pausing to recompile or refresh
## Providing HTTP Services
The `HttpModule` is not a core Angular module.
The `HttpModule` is not a core NgModule.
`HttpModule` is Angular's optional approach to web access. It exists as a separate add-on module called `@angular/http`
and is shipped in a separate script file as part of the Angular npm package.

View File

@ -31,6 +31,7 @@
"docs-watch": "node tools/transforms/authors-package/watchr.js",
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
"docs-test": "node tools/transforms/test.js",
"serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"",
"~~update-webdriver": "webdriver-manager update --standalone false --gecko false",
"boilerplate:add": "node ./tools/examples/add-example-boilerplate add",
"boilerplate:remove": "node ./tools/examples/add-example-boilerplate remove",
@ -47,32 +48,31 @@
},
"private": true,
"dependencies": {
"@angular/animations": "4.2.1",
"@angular/common": "4.2.1",
"@angular/compiler": "4.2.1",
"@angular/core": "4.2.1",
"@angular/forms": "4.2.1",
"@angular/http": "4.2.1",
"@angular/animations": "^4.2.4",
"@angular/common": "^4.2.4",
"@angular/compiler": "^4.2.4",
"@angular/core": "^4.2.4",
"@angular/forms": "^4.2.4",
"@angular/http": "^4.2.4",
"@angular/material": "^2.0.0-beta.7",
"@angular/platform-browser": "4.2.1",
"@angular/platform-browser-dynamic": "4.2.1",
"@angular/platform-server": "4.2.1",
"@angular/router": "4.2.1",
"@angular/platform-browser": "^4.2.4",
"@angular/platform-browser-dynamic": "^4.2.4",
"@angular/platform-server": "^4.2.4",
"@angular/router": "^4.2.4",
"@angular/service-worker": "^1.0.0-beta.16",
"classlist.js": "^1.1.20150312",
"core-js": "^2.4.1",
"jasmine": "^2.6.0",
"ng-pwa-tools": "^0.0.10",
"ngo-loader": "alxhub/ngo",
"purify": "igorminar/purify",
"ngo": "angular/ngo",
"rxjs": "^5.2.0",
"tslib": "^1.7.1",
"web-animations-js": "^2.2.5",
"zone.js": "^0.8.4"
"zone.js": "^0.8.12"
},
"devDependencies": {
"@angular/cli": "angular/cli-builds#webpack-next",
"@angular/compiler-cli": "4.2.1",
"@angular/compiler-cli": "^4.2.4",
"@types/jasmine": "^2.5.52",
"@types/node": "~6.0.60",
"archiver": "^1.3.0",
@ -81,7 +81,7 @@
"concurrently": "^3.4.0",
"cross-spawn": "^5.1.0",
"dgeni": "^0.4.7",
"dgeni-packages": "^0.19.1",
"dgeni-packages": "^0.20.0-rc.5",
"entities": "^1.1.1",
"eslint": "^3.19.0",
"eslint-plugin-jasmine": "^2.2.0",

View File

@ -3,8 +3,8 @@
set -u -e -o pipefail
declare -A limitUncompressed
limitUncompressed=(["inline"]=1600 ["main"]=600000 ["polyfills"]=40000)
limitUncompressed=(["inline"]=1600 ["main"]=600000 ["polyfills"]=35000)
declare -A limitGzip7
limitGzip7=(["inline"]=1000 ["main"]=140000 ["polyfills"]=12000)
limitGzip7=(["inline"]=1000 ["main"]=140000 ["polyfills"]=12500)
declare -A limitGzip9
limitGzip9=(["inline"]=1000 ["main"]=140000 ["polyfills"]=12000)
limitGzip9=(["inline"]=1000 ["main"]=140000 ["polyfills"]=12500)

View File

@ -9,7 +9,9 @@ readonly INPUT_DIR=dist/
readonly OUTPUT_FILE=/tmp/snapshot.tar.gz
readonly AIO_BUILDS_DOMAIN=ngbuilds.io
readonly UPLOAD_URL=https://$AIO_BUILDS_DOMAIN/create-build/$TRAVIS_PULL_REQUEST/$TRAVIS_PULL_REQUEST_SHA
readonly DEPLOYED_URL=https://pr$TRAVIS_PULL_REQUEST-$TRAVIS_PULL_REQUEST_SHA.$AIO_BUILDS_DOMAIN
readonly SHORT_SHA=$(echo $TRAVIS_PULL_REQUEST_SHA | cut -c1-7)
readonly DEPLOYED_URL=https://pr$TRAVIS_PULL_REQUEST-$SHORT_SHA.$AIO_BUILDS_DOMAIN
readonly skipBuild=$([[ "$1" == "--skip-build" ]] && echo "true" || echo "");
readonly relevantChangedFilesCount=$(git diff --name-only $TRAVIS_COMMIT_RANGE | grep -P "^(?:aio|packages)/(?!.*[._]spec\.[jt]s$)" | wc -l)
@ -49,6 +51,6 @@ readonly relevantChangedFilesCount=$(git diff --name-only $TRAVIS_COMMIT_RANGE |
# Run PWA-score tests (unless the deployment is not public yet;
# i.e. it could not be automatically verified).
if [ $httpCode -ne 202 ]; then
yarn test-pwa-score -- "$DEPLOYED_URL" "$MIN_PWA_SCORE" "$PWA_RESULTS_LOG"
yarn test-pwa-score -- "$DEPLOYED_URL" "$MIN_PWA_SCORE"
fi
)

View File

@ -25,6 +25,14 @@ case $deployEnv in
;;
esac
# Do not deploy if the current commit is not the latest on its branch.
readonly LATEST_COMMIT=$(git ls-remote origin $TRAVIS_BRANCH | cut -c1-40)
if [ $TRAVIS_COMMIT != $LATEST_COMMIT ]; then
echo "Skipping deploy because $TRAVIS_COMMIT is not the latest commit ($LATEST_COMMIT)."
exit 0
fi
# Deploy
(
cd "`dirname $0`/.."
@ -39,5 +47,5 @@ esac
firebase deploy --message "Commit: $TRAVIS_COMMIT" --non-interactive --token "$firebaseToken"
# Run PWA-score tests
yarn test-pwa-score -- "$deployedUrl" "$MIN_PWA_SCORE" "$PWA_RESULTS_LOG"
yarn test-pwa-score -- "$deployedUrl" "$MIN_PWA_SCORE"
)

View File

@ -4,10 +4,11 @@
set +x -eu -o pipefail
readonly thisDir=$(cd $(dirname $0); pwd)
readonly parentDir=$(dirname $thisDir)
readonly TOKEN=${ANGULAR_PAYLOAD_FIREBASE_TOKEN:-}
readonly PROJECT_NAME="angular-payload-size"
source scripts/_payload-limits.sh
source ${thisDir}/_payload-limits.sh
failed=false
payloadData=""
@ -43,8 +44,8 @@ payloadData="$payloadData\"timestamp\": $timestamp, "
# Add change source: application, dependencies, or 'application+dependencies'
yarnChanged=false
allChangedFiles=$(git diff --name-only $TRAVIS_COMMIT_RANGE $thisDir | wc -l)
allChangedFileNames=$(git diff --name-only $TRAVIS_COMMIT_RANGE $thisDir)
allChangedFiles=$(git diff --name-only $TRAVIS_COMMIT_RANGE $parentDir | wc -l)
allChangedFileNames=$(git diff --name-only $TRAVIS_COMMIT_RANGE $parentDir)
if [[ $allChangedFileNames == *"yarn.lock"* ]]; then
yarnChanged=true
@ -68,7 +69,9 @@ payloadData="{${payloadData}}"
echo $payloadData
if [[ "$TRAVIS_PULL_REQUEST" == "false" ]]; then
firebase database:update --data "$payloadData" --project $PROJECT_NAME --confirm --token "$TOKEN" /payload/aio/$TRAVIS_BRANCH/$TRAVIS_COMMIT
readonly safeBranchName=$(echo $TRAVIS_BRANCH | sed -e 's/\./_/g')
readonly dbPath=/payload/aio/$safeBranchName/$TRAVIS_COMMIT
firebase database:update --data "$payloadData" --project $PROJECT_NAME --confirm --token "$TOKEN" $dbPath
fi
if [[ $failed = true ]]; then

View File

@ -18,14 +18,12 @@ import { Logger } from 'app/shared/logger.service';
import { MockLocationService } from 'testing/location.service';
import { MockLogger } from 'testing/logger.service';
import { MockSearchService } from 'testing/search.service';
import { MockSwUpdateNotificationsService } from 'testing/sw-update-notifications.service';
import { NavigationNode } from 'app/navigation/navigation.service';
import { ScrollService } from 'app/shared/scroll.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchService } from 'app/search/search.service';
import { SelectComponent, Option } from 'app/shared/select/select.component';
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
import { TocComponent } from 'app/embedded/toc/toc.component';
import { TocItem, TocService } from 'app/shared/toc.service';
@ -68,13 +66,6 @@ describe('AppComponent', () => {
expect(component).toBeDefined();
});
describe('ServiceWorker update notifications', () => {
it('should be enabled', () => {
const swUpdateNotifications = TestBed.get(SwUpdateNotificationsService) as SwUpdateNotificationsService;
expect(swUpdateNotifications.enable).toHaveBeenCalled();
});
});
describe('hasFloatingToc', () => {
it('should initially be true', () => {
const fixture2 = TestBed.createComponent(AppComponent);
@ -904,7 +895,6 @@ function createTestingModule(initialUrl: string) {
{ provide: LocationService, useFactory: () => new MockLocationService(initialUrl) },
{ provide: Logger, useClass: MockLogger },
{ provide: SearchService, useClass: MockSearchService },
{ provide: SwUpdateNotificationsService, useClass: MockSwUpdateNotificationsService },
]
});
}

View File

@ -11,7 +11,6 @@ import { ScrollService } from 'app/shared/scroll.service';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchService } from 'app/search/search.service';
import { SwUpdateNotificationsService } from 'app/sw-updates/sw-update-notifications.service';
import { TocService } from 'app/shared/toc.service';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
@ -106,7 +105,6 @@ export class AppComponent implements OnInit {
private navigationService: NavigationService,
private scrollService: ScrollService,
private searchService: SearchService,
private swUpdateNotifications: SwUpdateNotificationsService,
private tocService: TocService
) { }
@ -177,8 +175,6 @@ export class AppComponent implements OnInit {
this.navigationService.versionInfo.subscribe( vi => this.versionInfo = vi );
this.swUpdateNotifications.enable();
const hasNonEmptyToc = this.tocService.tocList.map(tocList => tocList.length > 0);
combineLatest(hasNonEmptyToc, this.showFloatingToc)
.subscribe(([hasToc, showFloatingToc]) => this.hasFloatingToc = hasToc && showFloatingToc);
@ -277,7 +273,7 @@ export class AppComponent implements OnInit {
this.tocMaxHeightOffset =
el.querySelector('footer').clientHeight +
el.querySelector('md-toolbar.app-toolbar').clientHeight +
44; // margin
24; // fudge margin
}
this.tocMaxHeight = (document.body.scrollHeight - window.pageYOffset - this.tocMaxHeightOffset).toFixed(2);

View File

@ -1,7 +1,6 @@
/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement, Input } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement, ElementRef, Input, OnInit, ViewChild } from '@angular/core';
import { CodeExampleComponent } from './code-example.component';
@ -75,13 +74,12 @@ describe('CodeExampleComponent', () => {
});
//// Test helpers ////
// tslint:disable:member-ordering
@Component({
selector: 'aio-code',
template: `
<div>lang: {{language}}</div>
<div>linenums: {{linenums}}</div>
code: <pre>{{someCode}}</pre>
<div>lang: {{language}}</div>
<div>linenums: {{linenums}}</div>
code: <pre>{{someCode}}</pre>
`
})
class TestCodeComponent {

View File

@ -0,0 +1,245 @@
import { CommonModule } from '@angular/common';
import { Component, DebugElement, Input, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MdTabGroup, MdTabsModule } from '@angular/material';
import { By } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { CodeTabsComponent } from './code-tabs.component';
describe('CodeTabsComponent', () => {
let fixture: ComponentFixture<HostComponent>;
let hostComponent: HostComponent;
let codeTabsDe: DebugElement;
let codeTabsComponent: CodeTabsComponent;
const createComponentBasic = (codeTabsContent = '') => {
fixture = TestBed.createComponent(HostComponent);
hostComponent = fixture.componentInstance;
codeTabsDe = fixture.debugElement.children[0];
codeTabsComponent = codeTabsDe.componentInstance;
// Copy the CodeTab's innerHTML (content)
// into the `codeTabsContent` property as the DocViewer does.
codeTabsDe.nativeElement.codeTabsContent = codeTabsContent;
fixture.detectChanges();
};
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ CodeTabsComponent, HostComponent, TestCodeComponent ],
imports: [ CommonModule ],
schemas: [ NO_ERRORS_SCHEMA ],
});
});
it('should create CodeTabsComponent', () => {
createComponentBasic();
expect(codeTabsComponent).toBeTruthy('CodeTabsComponent');
});
describe('(tab labels)', () => {
let labelElems: HTMLSpanElement[];
const createComponent = (codeTabsContent?: string) => {
createComponentBasic(codeTabsContent);
const labelDes = codeTabsDe.queryAll(By.css('.mat-tab-label'));
labelElems = labelDes.map(de => de.nativeElement.querySelector('span'));
};
beforeEach(() => {
TestBed.configureTestingModule({
imports: [ MdTabsModule, NoopAnimationsModule ]
});
});
it('should create a label for each tab', () => {
createComponent(`
<code-pane>foo</code-pane>
<code-pane>bar</code-pane>
<code-pane>baz</code-pane>
`);
expect(labelElems.length).toBe(3);
});
it('should use the `title` as label', () => {
createComponent(`
<code-pane title="foo-title">foo</code-pane>
<code-pane title="bar-title">bar</code-pane>
`);
const texts = labelElems.map(s => s.textContent);
expect(texts).toEqual(['foo-title', 'bar-title']);
});
it('should add the `class` to the label element', () => {
createComponent(`
<code-pane class="foo-class">foo</code-pane>
<code-pane class="bar-class">bar</code-pane>
`);
const classes = labelElems.map(s => s.className);
expect(classes).toEqual(['foo-class', 'bar-class']);
});
it('should disable ripple effect on tab labels', () => {
createComponent();
const tabsGroupComponent = codeTabsDe.query(By.directive(MdTabGroup)).componentInstance;
expect(tabsGroupComponent.disableRipple).toBe(true);
});
});
describe('(tab content)', () => {
let codeDes: DebugElement[];
let codeComponents: TestCodeComponent[];
const createComponent = (codeTabsContent?: string) => {
createComponentBasic(codeTabsContent);
codeDes = codeTabsDe.queryAll(By.directive(TestCodeComponent));
codeComponents = codeDes.map(de => de.componentInstance);
};
it('should pass `class` to CodeComponent (<aio-code>)', () => {
createComponent(`
<code-pane class="foo-class">foo</code-pane>
<code-pane class="bar-class">bar</code-pane>
`);
const classes = codeDes.map(de => de.nativeElement.className);
expect(classes).toEqual(['foo-class', 'bar-class']);
});
it('should pass content to CodeComponent (<aio-code>)', () => {
createComponent(`
<code-pane>foo</code-pane>
<code-pane>bar</code-pane>
`);
const codes = codeComponents.map(c => c.code);
expect(codes).toEqual(['foo', 'bar']);
});
it('should pass `language` to CodeComponent (<aio-code>)', () => {
createComponent(`
<code-pane language="foo-lang">foo</code-pane>
<code-pane language="bar-lang">bar</code-pane>
`);
const langs = codeComponents.map(c => c.language);
expect(langs).toEqual(['foo-lang', 'bar-lang']);
});
it('should pass `linenums` to CodeComponent (<aio-code>)', () => {
createComponent(`
<code-pane linenums="foo-lnums">foo</code-pane>
<code-pane linenums="bar-lnums">bar</code-pane>
<code-pane linenums="">baz</code-pane>
<code-pane linenums>qux</code-pane>
`);
const lnums = codeComponents.map(c => c.linenums);
expect(lnums).toEqual(['foo-lnums', 'bar-lnums', '', '']);
});
it('should use the default value (if present on <code-tabs>) if `linenums` is not specified', () => {
TestBed.overrideComponent(HostComponent, {
set: { template: '<code-tabs linenums="default-lnums"></code-tabs>' }
});
createComponent(`
<code-pane linenums="foo-lnums">foo</code-pane>
<code-pane linenums>bar</code-pane>
<code-pane>baz</code-pane>
`);
const lnums = codeComponents.map(c => c.linenums);
expect(lnums).toEqual(['foo-lnums', '', 'default-lnums']);
});
it('should pass `path` to CodeComponent (<aio-code>)', () => {
createComponent(`
<code-pane path="foo-path">foo</code-pane>
<code-pane path="bar-path">bar</code-pane>
`);
const paths = codeComponents.map(c => c.path);
expect(paths).toEqual(['foo-path', 'bar-path']);
});
it('should default to an empty string if `path` is not spcified', () => {
createComponent(`
<code-pane>foo</code-pane>
<code-pane>bar</code-pane>
`);
const paths = codeComponents.map(c => c.path);
expect(paths).toEqual(['', '']);
});
it('should pass `region` to CodeComponent (<aio-code>)', () => {
createComponent(`
<code-pane region="foo-region">foo</code-pane>
<code-pane region="bar-region">bar</code-pane>
`);
const regions = codeComponents.map(c => c.region);
expect(regions).toEqual(['foo-region', 'bar-region']);
});
it('should default to an empty string if `region` is not spcified', () => {
createComponent(`
<code-pane>foo</code-pane>
<code-pane>bar</code-pane>
`);
const regions = codeComponents.map(c => c.region);
expect(regions).toEqual(['', '']);
});
it('should pass `title` to CodeComponent (<aio-code>)', () => {
createComponent(`
<code-pane title="foo-title">foo</code-pane>
<code-pane title="bar-title">bar</code-pane>
`);
const titles = codeComponents.map(c => c.title);
expect(titles).toEqual(['foo-title', 'bar-title']);
});
});
});
//// Test helpers ////
@Component({
selector: 'aio-code',
template: `
<div>lang: {{ language }}</div>
<div>linenums: {{ linenums }}</div>
code: <pre>{{ someCode }}</pre>
`
})
class TestCodeComponent {
@Input() code = '';
@Input() hideCopy: boolean;
@Input() language: string;
@Input() linenums: string;
@Input() path: string;
@Input() region: string;
@Input() title: string;
get someCode() {
if (this.code && this.code.length > 30) {
return `${this.code.substring(0, 30)}...`;
}
return this.code;
}
}
@Component({
selector: 'aio-host-comp',
template: `<code-tabs></code-tabs>`
})
class HostComponent {}

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