Compare commits

..

152 Commits
4.4.6 ... 4.2.5

Author SHA1 Message Date
61e6618429 docs: add changelog for 4.2.5 2017-06-29 17:17:00 -07:00
e1fea47400 release: cut the 4.2.5 release 2017-06-29 17:15:50 -07:00
f31b0d664d fix(core): add needed closure compiler warning suppression 2017-06-29 17:08:12 -07:00
79b634692a fix(animations): properly collect :enter nodes that exist within multi-level DOM trees
Closes #17632
2017-06-29 17:08:12 -07:00
6909171c0a fix(animations): do not validate style overlap errors in different transitions 2017-06-29 17:08:12 -07:00
ec4ae60fb8 fix(animations): do not remove container nodes when children are queried by a parent animation
Closes #17746
2017-06-29 17:08:12 -07:00
7559b7898e fix(animations): do not delay style() values before a stagger() runs
Closes #17412
2017-06-29 17:08:12 -07:00
7728d7efc6 ci: remove print-logs.sh to try to fix failing builds on Travis 2017-06-29 15:33:21 -07:00
2c1ef08466 docs: Updated router guide content and examples for paramMap
and queryParamMap, tracing, and incidental improvements.
closes #16991 and #16259 which it also fixes.
2017-06-29 15:29:51 -07:00
5f1dcfc228 feat(aio): use shorter URLs for previews
Use the 7 first characters of the 40-chars long SHAs for shorter/cleaner URLs.
The collision probability is extremely low (since all SHAs are further
"namespaced" under the corresponding PR). In case of a collision, the second PR
will not be deployed, in order to avoid overwriting the original build.

(This is a design decision to keep the implementation simple. It can be changed
later if necessary.)
2017-06-29 15:29:51 -07:00
f9f2b23afc fix(aio): clean up non-public previews
The previous clean-up code for PR directories on the preview server assumed that
all directories were named after the PR number. With the changes introduced
in #17640 it is possible to have PR directories that do not follow that naming
convention (e.g. "non-public" directories).

This PR ensures that both public and non-public directories are removed when
cleaning up.
2017-06-29 15:29:51 -07:00
ad3661cff7 feat(aio): cross reference readme and docs styleguide 2017-06-29 15:29:51 -07:00
804173c8a6 docs(aio): update punctuation mark added in TOOLS guide 2017-06-29 15:29:51 -07:00
a0c790e7bd docs: correct grammar in CONTRIBUTING.md 2017-06-29 15:29:51 -07:00
7205d7a568 fix(aio): build scripts-js before creating a new docker image for the preview server
When creating a new docker image for the preview server, the TypeScript source
code in `scripts-js/` is not copied over. Instead only the generated JavaScript
core in `scripts-js/dist/` are. Because of that, it is necessary to have run
`yarn build` before running `docker build`, so that the new docker image
contains the latest changes in `scripts-js/`.

This was previously part of the `create-image.sh` script, but was accidentally
removed in 21d213dfc.
2017-06-27 15:57:50 -04:00
e57eec9b13 ci: add npm postinstall back to the lint step so node_modules doesn't get out of date 2017-06-27 15:57:44 -04:00
5e1b339f46 docs: fix spelling of case variants in naming.md
unify variant forms of spelling letter cases (upper-case, uppercase, lower case)
2017-06-27 15:57:38 -04:00
6b23e2f427 build(aio): boilerplate wont be removed by default now 2017-06-27 15:57:32 -04:00
08dba3f93b fix(aio): Typo in Setup Anatomy documentation page
Solves #17076
2017-06-27 15:57:27 -04:00
2624fa5ae2 docs(aio): fix typo 2017-06-27 15:57:22 -04:00
a1458d9cb9 docs(aio): animations typos fixed 2017-06-27 15:57:17 -04:00
ffbdf74e92 docs(aio): cleanup rollup-config script 2017-06-23 12:11:57 -07:00
589826f5e5 ci(aio): Change the firebase token 2017-06-23 12:11:56 -07:00
6f5e6374f8 ci(aio): address comments 2017-06-23 12:11:56 -07:00
a309d62806 ci(aio): fix test 2017-06-23 12:11:56 -07:00
9699c02642 ci(aio): remove umd 2017-06-23 12:11:56 -07:00
6040c66e95 ci(aio): debug 2017-06-23 12:11:55 -07:00
347fec5e8c ci(aio): add back deploy-preview 2017-06-23 12:11:55 -07:00
c7bbf0573a ci(aio): updated limits 2017-06-23 12:11:55 -07:00
c2fda10a73 ci(aio): rename limits file and address comments 2017-06-23 12:11:55 -07:00
b38edeb055 ci(aio): Also track umd.min.js file size 2017-06-23 12:11:54 -07:00
2407beb17e ci(aio): Add payload size limit file 2017-06-23 12:11:54 -07:00
b6418039e0 ci(aio): upload aio payload size to firebase
ci(aio): Add timestamp and change data
2017-06-23 12:11:54 -07:00
7dd7722220 build: No longer need to bazel build twice 2017-06-23 12:11:54 -07:00
9b14f62387 build: circleci workflows to run lint in parallel 2017-06-23 12:11:54 -07:00
107655e43f refactor(aio): provide fallback values for secrets (useful during dev) 2017-06-23 12:11:53 -07:00
162bddf0b8 refactor(aio): enable -u flag on preview server scripts 2017-06-23 12:11:53 -07:00
81c18b7241 docs(aio): document preview server HTTP status codes 2017-06-23 12:11:53 -07:00
0c4b561999 ci(aio): deploy previews for all PRs
PRs that could not be automatically verified will not be publicly accessible,
until manually verified.
2017-06-23 12:11:53 -07:00
563577eeab test(aio): add e2e tests for non-public previews 2017-06-23 12:11:53 -07:00
17e000a678 feat(aio): enable previews for any PR
This commit introduces the ability to show previews for PRs by any author. It works as follows:

- The build artifacts of all PRs are uploaded to the preview server.
- Automatically verified PRs (i.e. from trusted authors or having a specific label) are deployed and
  publicly accessible as usual.
- PRs that could not be automatically verified are stored for later use (after re-verification).
- A PR can be marked as "trusted" and make its preview publicly accessible by adding the GitHub
  label specified in the `AIO_TRUSTED_PR_LABEL` env var of the preview server.

At the moment, there is no automatic mechanism for notifying the preview server about changes to the
PR's verification status. The PR's "visibility" will be checked and updated every time a new build
is uploaded.
2017-06-23 12:11:52 -07:00
b8806a656f refactor(aio): simplify preview server build events 2017-06-23 12:11:52 -07:00
ee5cd52d5f test(aio): add missing unit test for preview server 2017-06-23 12:11:52 -07:00
84a72b774e test(aio): fix preview server tests on Windows 2017-06-23 12:11:52 -07:00
9e28f58ec8 build(aio): upgrade preview server dependencies 2017-06-23 12:11:51 -07:00
10e2db000c fix(aio): prefix location.assign with window.
No practical effect but clears the TS compiler warning.
2017-06-23 12:11:51 -07:00
098d35ef64 fix(aio): fix links on /about in Firefox
Fixes #17661
2017-06-23 12:11:51 -07:00
d5a8f6ebfe refactor(aio): clean up aio-contributor template and styles 2017-06-23 12:11:51 -07:00
9415e6603f fix(aio): preserve newlines when copying code
Before 4f37f8643, we were using `innerText` to retrieved the code content for
copying. This preserved the text layout (including newlines), but suffered from
other issues (browser support, performance). With 4f37f8643 we switched to
`textContent`, which works well except in the following case:
When `prettify` formats the code to have line numbers, it removes the newlines
and uses `<li>` elements instead. This affects `textContent`.

This commit fixes this by keeping a reference of the code as text and using that
for copying.

Fixes #17659
2017-06-23 12:11:50 -07:00
927ea2462e ci: test merge commits on circle
We expect this behavior because it's what Travis does. Also it's better because we want
to test what happens if we merge the PR, not the status of the PR branch.
2017-06-23 12:11:50 -07:00
1d6465b596 docs(aio): Update resources.json - fixed language error 2017-06-23 12:11:50 -07:00
58012df46f docs(aio): update resources - Add Compodoc - documentation tool for Angular app 2017-06-23 12:11:50 -07:00
21c5b177ef fix(aio): fix topbar nav-item focus style
Fixing it requires upgrading `@angular/material` to v2.0.0-beta.7.

Fixes #17216
2017-06-23 12:11:50 -07:00
bc10291d42 docs: remove unnecessary newline 2017-06-23 12:11:49 -07:00
9344c86242 docs: fix “under to hood” typo in changelog.md
closes #14856
2017-06-23 13:18:45 -04:00
6b5b3a7269 docs(aio): resources.json - replace “Angular 2” in titles/descriptions
closes #16965
2017-06-23 13:18:41 -04:00
58c5e86209 docs(aio): update Bash for Windows info
As of the Creator's Update, Bash on Ubuntu on Windows supports all npm commands. 
https://blogs.msdn.microsoft.com/commandline/2017/04/11/windows-10-creators-update-whats-new-in-bashwsl-windows-console/
Update setup documentation accordingly.
2017-06-23 13:18:37 -04:00
1657fa756f docs(aio): minor fixes for the upgrade guide
Fixes #17093
2017-06-23 13:18:33 -04:00
2ff85c6790 fix(aio): add missing WeakMap polyfill 2017-06-23 13:18:26 -04:00
353de5191c docs(aio): replace “Angular 1” in upgrade phonecat script 2017-06-22 23:26:08 -04:00
ea5ac89b50 docs(aio): fix promises link in toh-pt4
closes #16050
2017-06-22 23:26:03 -04:00
f5ce60e6bc docs(aio): remove "_" from private property name
follow best practices from the documentation: https://angular.io/guide/styleguide#properties-and-methods
2017-06-22 23:25:58 -04:00
7a2903993a docs(aio): correct typos in Tour of Heroes HeroSearchService 2017-06-22 23:25:53 -04:00
a29a0df183 docs(aio): Added resources to UI Components (#17635)
docs(aio): Added resources to UI Components
2017-06-22 23:25:48 -04:00
746fed453a docs(aio): remove min, max & move built-in validators paragraph 2017-06-22 23:25:43 -04:00
64e1b9cc9a docs(aio): added link to Russian Angular Courses 2017-06-22 23:25:39 -04:00
0947f4a358 docs: add documentation for LTS versions 2017-06-22 23:25:35 -04:00
f10d5a5572 docs: add missing colon in the Constants section of NAMING.md 2017-06-22 23:25:30 -04:00
93f57d7d70 docs: Fix Stack Overflow being repeatedly misspelt as a single word
(To see that it is two words, see e.g. the page title at https://stackoverflow.com/ or the
spelling at http://stackoverflow.com/tour)
2017-06-22 23:24:31 -04:00
af1c4db0e4 docs: Fix some non-code being formatted as code in the router docs 2017-06-22 23:24:18 -04:00
4f43029570 docs: add changelog for 4.3.0-beta.0 2017-06-22 23:24:06 -04:00
8a09015211 docs: add changelog for 4.2.4 2017-06-21 17:01:14 -07:00
f4b7bf7e38 release: cut the 4.2.4 release 2017-06-21 16:57:31 -07:00
d20313698d revert: feat(core): update zone.js to 0.8.12
This reverts commit 25234227ec.

Mistakenly cherry picked a feature commit which does not belong on a
patch branch.
2017-06-21 16:52:14 -07:00
74954f1a8d docs(aio): ward's changes 2017-06-21 16:28:19 -07:00
368aed087c docs(aio): change to overall prose 2017-06-21 16:28:19 -07:00
975341a77e docs(aio): create author style guide 2017-06-21 16:28:19 -07:00
c112232fa3 fix(compiler): avoid emitting self importing factories
Fixes: #17389
2017-06-21 16:20:05 -07:00
36348325de feat(aio): display “Searching ..." while building search index
closes #15923
2017-06-21 14:31:59 -07:00
fdbe62f112 fix(aio): fix patch script on windows
Running the patch script on Windows (with `patch` available) yields an invalid syntax warning, and does not apply patches.

```
kamik@T460p MINGW64 /d/work/angular/aio (master)
$ yarn postinstall
yarn postinstall v0.24.6
$ node tools/cli-patches/patch.js && uglifyjs node_modules/lunr/lunr.js -c -m -o src/assets/js/lunr.min.js --source-map
The syntax of the command is incorrect.
Done in 1.52s.
```
2017-06-21 13:51:47 -07:00
af9ba91e5c build(aio): ensure all doc tests are run
It is not possible to run all the docs tests directly via the  jasmine CLI.
Instead we now have a small script that will run jasmine via its library.
2017-06-21 13:51:27 -07:00
b840ec3684 feat(aio): select contributor group with URL “about?group=gde”
closes #17656
also adds test for ContributorListComponent.
2017-06-21 13:51:09 -07:00
b38552f36e fix(aio): restore component-styles/exclude hidden children 2017-06-21 11:33:33 -07:00
572885dcc8 docs(aio): fix numbered list in testing.md
This list renders OK in the github UI but not at https://angular.io/guide/testing#setup
2017-06-20 16:43:46 -07:00
33906489ad fix(tsc-wrapped): skip collecting metadata for default functions
Fixes: #17518
2017-06-20 14:23:04 -07:00
59299de374 fix(compiler-cli): find lazy routes in nested module import arrays
Fixes: #17531
2017-06-20 14:21:35 -07:00
f99793700f docs(aio): update resource listing 2017-06-20 12:58:20 -07:00
77860a0023 fix: argument destructuring sometimes breaks strictNullChecks
Destructuring of the form:

function foo({a, b}: {a?, b?} = {})

breaks strictNullChecks, due to the TypeScript bug https://github.com/microsoft/typescript/issues/10078.
This change eliminates usage of destructuring in function argument lists in cases where it would leak
into the public API .d.ts.
2017-06-20 12:56:21 -07:00
93d834a0b2 refactor(core): remove toString() method from DefaultKeyValueDiffer
toString() from DefaultKeyValueDiffer is only used in tests and should not
be part of the production code. toString() methods from differs add
~ 0.3KB (min+gzip) to the production bundle size.
2017-06-20 12:55:35 -07:00
63a5f33e99 fix(language-service): infer any ngForOf of type any
Fixes: #17611
2017-06-20 12:05:10 -07:00
20eb5cfd59 fix(language-service): rollup tslib into the language service package
Fixes: #17614
2017-06-20 11:50:42 -07:00
4ab7353a9f fix(forms): roll back breaking change with min/max directives
With 4.2, we introduced the min and max validator directives. This was actually a breaking change because their selectors could include custom value accessors using the min/max properties for their own purposes.

For now, we are rolling back the change by removing the exports. At the least, we should wait to add them until a major version. In the meantime, we will have further discussion about what the best solution is going forward for all validator directives.

Closes #17491.

----

PR #17551 tried to roll this back, but did not remove the dead code. This failed internal tests that were checking that all declared directives were used.
This PR rolls back the original PR and commit the same as #17551 while also removing the dead code.
2017-06-20 09:08:25 -07:00
eb23460e09 revert: fix(forms): temp roll back breaking change with min/max directives
This reverts commit 232bd9395d.
2017-06-20 09:08:25 -07:00
2e8efdaffa fix(aio): leave results panel open when opening search result on new page
Fixes #17580
2017-06-19 15:12:57 -07:00
3cfb13f746 build(aio): upgrade jasmine to v2.6.4
This version fixes the DISCONNECTED errors (described in #17543) and removes the
need to the workaround (8af203c).
The relevant jasmine commit is jasmine/jasmine@c60d66994.
2017-06-19 15:12:38 -07:00
7de1ae2be8 fix(router): update the version placeholder so that it gets replaced during the build
Fixes #17403
2017-06-19 15:11:22 -07:00
341b812a10 docs(TRIAGE_AND_LABELS): update labels to reflect the current state 2017-06-19 14:54:15 -07:00
09512c7770 docs(RELEASE_SCHEDULE): fix version numbers for August/September releases 2017-06-19 14:46:49 -07:00
1ec0a53fec docs(RELEASE_SCHEDULE): update the release schedule w/ recent regression patch releases 2017-06-19 13:20:53 -07:00
3607a48a08 docs: remove aio from changelog 2017-06-19 11:28:17 -07:00
62957fa515 fix(aio): switch from innerText to textContent to support older browsers
`innerText` is not supported in Firefox prior to v45. In most cases (at least
the ones we are interested in), `innerText` and `textContent` work equally well,
but `textContent` is more performant (as it doesn't require a reflow).

From [MDN][1] on the differences of `innerText` vs `textContent`:

> - [...]
> - `innerText` is aware of style and will not return the text of hidden
>   elements, whereas `textContent` will.
> - As `innerText` is aware of CSS styling, it will trigger a reflow, whereas
>   `textContent` will not.
> - [...]

[1]: https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent#Differences_from_innerText

Fixes #17585
2017-06-19 10:32:58 -07:00
ab81c1c068 build(aio): update @angular/service-worker to 1.0.0-beta.16
This version includes a fix for potential cache corruption and invalid redirect behavior in rare cases.
2017-06-16 13:34:00 -07:00
25234227ec feat(core): update zone.js to 0.8.12 2017-06-16 12:13:31 -07:00
71a8ef5a15 fix(aio): improve no-javascript screen 2017-06-16 12:05:03 -07:00
4dd6deca16 fix(aio): remove unused icon reference and file
These icons are not there (and never were afaict).

Fixes #17561
2017-06-16 12:04:15 -07:00
f8db05ef7d fix(aio): correctly redirect /docs/ts/latest and /styleguide
Previously, we had redirect rules for Firebase for `/docs/ts/latest` and
`/styleguide`, but once the ServiceWorker was activated, it would take over
routing and rewrite these requests to `/index.html`.
This commit fixes it by excluding them from ServiceWorker routing.

Fixes #17542
2017-06-16 12:03:24 -07:00
623b9c6e58 test(compiler): fix typo "mamespace" 2017-06-16 11:17:49 -07:00
8a547eeee0 docs: add changelog for 4.2.3 2017-06-16 09:41:51 -07:00
4211432fc8 release: cut the 4.2.3 release 2017-06-16 09:38:29 -07:00
b8c39cdf71 fix(forms): temp roll back breaking change with min/max directives
With 4.2, we introduced the min and max validator directives. This was actually a breaking change because
their selectors could include custom value accessors using the min/max properties for their own purposes.

For now, we are rolling back the change by removing the exports.

Closes #17491.
2017-06-16 09:32:19 -07:00
9c7a84de51 docs(aio): re-add biography entry for devversion
With SHA 2c3e948e61 the biography of Paul Gschwendtner has been accidentally removed.

This re-adds the biography entry (picture still present) as requested on Slack.
2017-06-16 07:56:30 +01:00
dbc6a4cb12 build(aio): do not fail if check-env for the main angular project fails
Fixes #17434
2017-06-16 07:53:26 +01:00
784410e3c8 build: fix link to DEVELOPER.md in check-environment.js 2017-06-16 07:53:25 +01:00
90a5a1ef43 build(aio): remove dependency on build artifacts from parent folder 2017-06-16 07:53:25 +01:00
301f99cd6c build: remove redundant line
The same value is set a few lines below.
2017-06-16 07:53:25 +01:00
64e63b9422 fix(aio): add missing redirect rule for /styleguide
Fixes #17542
2017-06-16 07:52:29 +01:00
b192dd5761 fix(animations): remove duplicate license header 2017-06-15 14:51:40 -07:00
96aa3bb135 docs(aio): update about page 2017-06-15 22:15:59 +01:00
8abc1df2c1 feat(aio): add iphone pwa features 2017-06-15 22:15:59 +01:00
f5eb528a5c docs(aio): incorporate Ward's comments 2017-06-15 22:15:58 +01:00
5cf06a9f3f docs(aio): add short section on built-in validators, copy edits 2017-06-15 22:15:58 +01:00
ab90f63575 fix(aio): do not log messages in production
In dev mode, all messages passed to `Logger` will be logged.
In production mode, only warnings and errors will be logged.

Fixes #17453
2017-06-15 22:15:57 +01:00
150d271f79 refactor(aio): remove unused Logger dependencies 2017-06-15 22:15:57 +01:00
a686eb2c9e build(aio): extra redirect rule 2017-06-15 22:15:57 +01:00
bfa788935a docs(aio): http guide shows how to import toPromise operator from rxjs
Solves #17454
2017-06-15 22:15:56 +01:00
64fa100a71 fix(aio): specify large image for PWA splash-screen 2017-06-14 09:45:37 -07:00
5fae987bfa build(aio): upgrade lighthouse to v2.1 2017-06-14 09:45:37 -07:00
9c4cda1c7d ci(aio): fail the build if the PWA score is too low
Previously, there was an issue with testing the PWA score on staging and failing
the build was temporarily disabled. It works now, so we need to enable failing
the build is the score drops below some threshold.
2017-06-14 09:45:37 -07:00
d9cbe56b63 test(aio): add async beforeEach to prevent Chrome disconnects
Related to 3d5f520ff0 from #17405
2017-06-14 09:45:37 -07:00
86df7108b0 fix(aio): always cover the whole footer with its background
Fixes #17465
2017-06-14 09:45:37 -07:00
e7a4f92be7 fix(aio): fix trackBy demo in template-syntax article 2017-06-14 09:45:36 -07:00
76af452d29 test(platform-server): fix and re-enable integration tests 2017-06-14 09:45:36 -07:00
11dfb685f4 ci: update github templates (#17466) 2017-06-13 15:27:35 -07:00
d363aa0aa4 fix(aio): make the footer links clickable on all browsers
The footer background (implemented via `footer:after`) had a higher `z-index`
than other footer elements and was obscuring the footer links on certain
browsers (Firefox, Edge, IE), which made them unclickable.
This commit lowers the index of `footer:after`, so that links are clickable on
these browsers.

Fixes #17460
2017-06-13 15:27:35 -07:00
209d74c342 ci: disable platform-server integration test as it is currently broken 2017-06-13 15:27:35 -07:00
fec8f6febe refactor(compiler): remove duplicate code 2017-06-13 15:27:35 -07:00
ee9daaf4c8 ci: use npm_install for bazel
using yarn_install polluted our node_modules cache because it disregards the npm_shrinkwrap.json
2017-06-13 15:27:35 -07:00
4164369db4 docs(aio): update typescript for examples/webpack to same as cli 2017-06-13 11:35:17 -07:00
747b6a61b8 build(aio): add staging environment
You can now specify what environment you are building
by add it to the `yarn build` command. For example:

```
yarn build -- --env=stage
```

Moreover the `deploy-to-firebase.sh` script will automatically apply the
appropriate environment.
2017-06-13 11:35:17 -07:00
9ed836c939 docs(aio): i18n guide - updates for v4 2017-06-13 11:35:17 -07:00
e4c82443f9 build(aio): increase docs integration test timeouts
The API docs tests have very variable run times, depending
upon the build environment.
This change doubles their test timeout values to prevent
false-negative failures.
2017-06-13 11:35:17 -07:00
a2f232166b fix(aio): fix scrolling to elements near the bottom of the page
Previously, we always assumed that elements would be scrolled to the top of the
page, when calling `element.scrollIntoView()`. This is not true for elements
that cannot be scrolled to the top, e.g. when the viewport height is larger than
the height of the content after the element (common for small sections near the
end of the page).
In such cases, we would unnecessarily scroll up to account for the static
toolbar, which was unnecessary (since the element was not behind the toolbar
anyway) and caused ScrollSpy to fail to identify the scrolled-to section as
active.

This commit fixes it by ensuring that we do not scroll more than necessary in
order to align the top of the element with the bottom of the toolbar.

Fixes #17452
2017-06-13 11:35:17 -07:00
668f9ede65 fix(aio): show search results when search box gets focus
Due to a previous commit, the search was only triggered
if the query changed, and not when the search box regained
focus.
2017-06-13 11:35:17 -07:00
b784829512 fix(aio): use locally hosted lunr library
The library is downloaded from npm but then
copied into the assets folder (and ignored by git)
as part of the postinstall step.
2017-06-13 11:35:17 -07:00
ad4fee7053 fix(aio): make search results better
* update to latest version of lunr search
* add trailing wildcard to search terms to increase matches
* fix unwanted error when escape was pressed

Closes #17417
2017-06-13 11:35:17 -07:00
2d31e17251 fix(aio): fix buttons in "Home" and "Features"
Using `<a>` inside a `<button>` is not syntactically valid HTML and breaks on
some browsers (e.g. Firefox). Furthermore, clicking the button doesn't do
anything unless you click on the link (e.g. clicking on the padding around the
link does nothing), which is inconvenient and confusing.

Fixes #17448
2017-06-13 11:35:17 -07:00
39cff565ee docs: clarify when non-null-assertion-operator is needed in template-syntax 2017-06-13 11:35:17 -07:00
203c5ba1b3 fix(aio): ensure that API filter page can display 3 columns in wide view
Fixes #17251
2017-06-13 11:35:17 -07:00
eda7bb5c3e fix(aio): tidy up layout of api filter page
* Remove the "info-banner" styling from the filters.
* Fix alignment of the search box on a narrow screen (closes #17395)
* Remove unnecessary whitespace before section headers
2017-06-13 11:35:17 -07:00
1480a30050 docs(aio): rename Upgrade docs to cheatsheet 2017-06-13 11:35:16 -07:00
d6087f75e2 fix(aio): remove outline from search input on focus
Closes #17396
2017-06-13 11:35:16 -07:00
dc084a5bdf build(aio): make deploy-to-firebase.sh executable 2017-06-12 16:13:46 -07:00
1058 changed files with 32688 additions and 38314 deletions

View File

@ -1,49 +1,36 @@
# 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
defaults: &defaults
working_directory: ~/ng
docker:
- image: angular/ngcontainer:0.0.2
# 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"
- image: angular/ngcontainer
version: 2
jobs:
lint:
<<: *job_defaults
<<: *defaults
steps:
- checkout:
<<: *post_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"
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
- run: yarn install --freeze-lockfile --non-interactive
- run: npm install
- run: npm run postinstall
- run: ./node_modules/.bin/gulp lint
build:
<<: *job_defaults
<<: *defaults
steps:
- checkout:
<<: *post_checkout
- checkout
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
- run: bazel run @yarn//:yarn
- run: bazel run @io_bazel_rules_typescript_node//:bin/npm install
- run: bazel build ...
- save_cache:
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
key: angular-{{ .Branch }}-{{ checksum "npm-shrinkwrap.json" }}
paths:
- "node_modules"

View File

@ -1,14 +1,14 @@
<!--
PLEASE HELP US PROCESS GITHUB ISSUES FASTER BY PROVIDING THE FOLLOWING INFORMATION.
ISSUES MISSING IMPORTANT INFORMATION MAY BE CLOSED WITHOUT INVESTIGATION.
ISSUES MISSING IMPORTANT INFORMATION MIGHT BE CLOSED WITHOUT INVESTIGATION.
-->
## I'm submitting a...
## I'm submitting a ...
<!-- Check one of the following options with "x" -->
<pre><code>
[ ] 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 -->
[ ] 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 -->
[ ] 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. -->
## Environment
## Please tell us about your environment
<pre><code>
Angular version: X.Y.Z
@ -49,8 +49,8 @@ Browser:
- [ ] Edge version XX
For Tooling issues:
- Node version: XX <!-- run `node --version` -->
- Platform: <!-- Mac, Linux, Windows -->
- Node version: XX <!-- use `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
Please check if your PR fulfills the following requirements:
Does 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,7 +2,6 @@
/dist/
bazel-*
e2e_test.*
node_modules
bower_components

View File

@ -1 +0,0 @@
Michał Gołębiowski-Owczarek <m.goleb@gmail.com>

View File

@ -8,11 +8,9 @@
# 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
@ -20,11 +18,10 @@
# 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
@ -96,7 +93,6 @@ groups:
- "packages/core/*"
users:
- tbosch #primary
- chuckjaz
- mhevery
- vicb
- IgorMinar #fallback
@ -104,11 +100,10 @@ groups:
animations:
conditions:
files:
- "packages/animations/*"
- "packages/animation/*"
- "packages/platform-browser/animations/*"
users:
- matsko #primary
- chuckjaz #fallback
- mhevery #fallback
- IgorMinar #fallback
@ -254,46 +249,10 @@ groups:
angular.io:
conditions:
files:
include:
- "aio/*"
exclude:
- "aio/content/*"
- "aio/*"
users:
- petebacondarwin #primary
- IgorMinar
- IgorMinar #primary
- petebacondarwin #secondary
- gkalpak
- 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
- petebacondarwin
- gkalpak
- 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
- petebacondarwin
- gkalpak
- IgorMinar #fallback
- mhevery #fallback

View File

@ -1,12 +1,9 @@
language: node_js
sudo: false
# force trusty as Google Chrome addon is not supported on Precise
dist: trusty
node_js:
- '6.9.5'
addons:
chrome: stable
# firefox: "38.0"
apt:
sources:
@ -50,21 +47,19 @@ env:
- CI_MODE=e2e_2
- CI_MODE=js
- CI_MODE=saucelabs_required
# deactivated, see #19768
# - CI_MODE=browserstack_required
- CI_MODE=browserstack_required
- CI_MODE=saucelabs_optional
- CI_MODE=browserstack_optional
- CI_MODE=aio_tools_test
- CI_MODE=docs_test
- CI_MODE=aio
- CI_MODE=aio_e2e AIO_SHARD=0
- CI_MODE=aio_e2e AIO_SHARD=1
- CI_MODE=bazel
- CI_MODE=aio_e2e
matrix:
fast_finish: true
allow_failures:
- env: "CI_MODE=saucelabs_optional"
- env: "CI_MODE=browserstack_optional"
- env: "CI_MODE=aio_e2e"
before_install:
# source the env.sh script so that the exported variables are available to other scripts later on
@ -81,4 +76,5 @@ 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
- ./scripts/ci/print-logs.sh
# Disabled so we can debug Travis build failures on Master seeming to coming from printing logs
# - ./scripts/ci/print-logs.sh

View File

@ -11,15 +11,8 @@ filegroup(
# This won't scale in the general case.
# TODO(alexeagle): figure out what to do
"node_modules/typescript/**",
"node_modules/zone.js/**",
"node_modules/zone.js/**/*.d.ts",
"node_modules/rxjs/**/*.d.ts",
"node_modules/rxjs/**/*.js",
"node_modules/@types/**/*.d.ts",
"node_modules/tsickle/**",
"node_modules/hammerjs/**/*.d.ts",
"node_modules/protobufjs/**",
"node_modules/bytebuffer/**",
"node_modules/reflect-metadata/**",
"node_modules/minimist/**/*.js",
]),
)

File diff suppressed because it is too large Load Diff

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?
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`.
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`.
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 systematically close all issues that are requests for general support and redirect people to Stack Overflow.
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.
If you would like to chat about the question in real-time, you can reach out via [our gitter channel][gitter].
@ -206,7 +206,6 @@ 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,21 +3,22 @@
[![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://www.npmjs.com/@angular/core)
[![npm version](https://badge.fury.io/js/%40angular%2Fcore.svg)](https://badge.fury.io/js/%40angular%2Fcore)
[![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
=========
Angular is a development platform for building mobile and desktop web applications using Typescript/JavaScript (JS) and other languages.
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

@ -1,17 +1,11 @@
load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
git_repository(
name = "build_bazel_rules_typescript",
name = "io_bazel_rules_typescript",
remote = "https://github.com/bazelbuild/rules_typescript.git",
tag = "0.0.5",
commit = "3a8404d",
)
load("@build_bazel_rules_typescript//:defs.bzl", "node_repositories")
load("@io_bazel_rules_typescript//:defs.bzl", "node_repositories")
node_repositories(package_json = "//:package.json")
git_repository(
name = "build_bazel_rules_angular",
remote = "https://github.com/bazelbuild/rules_angular.git",
tag = "0.0.1",
)

View File

@ -31,9 +31,8 @@
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"next": "environments/environment.next.ts",
"stable": "environments/environment.stable.ts",
"archive": "environments/environment.archive.ts"
"stage": "environments/environment.stage.ts",
"prod": "environments/environment.prod.ts"
}
}
],

View File

@ -13,14 +13,9 @@ You should run all these tasks from the `angular/aio` folder.
Here are the most important tasks you might need to use:
* `yarn` - install all the dependencies.
* `yarn setup` - install all the dependencies, boilerplate, plunkers, zips and run dgeni on the docs.
* `yarn setup-local` - same as `setup`, but use the locally built Angular packages for aio and docs examples boilerplate.
* `yarn build` - create a production build of the application (after installing dependencies, boilerplate, etc).
* `yarn build-local` - same as `build`, but use `setup-local` instead of `setup`.
* `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,33 +25,28 @@ Here are the most important tasks you might need to use:
* `yarn docs-lint` - check that the doc gen code follows our style rules.
* `yarn docs-test` - run the unit tests for the doc generation code.
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally. Add the option `--local` to use your local version of Angular contained in the "dist" folder.
* `yarn boilerplate:add` - generate all the boilerplate code for the examples, so that they can be run locally.
* `yarn boilerplate:remove` - remove all the boilerplate code that was added via `yarn boilerplate:add`.
* `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 example-e2e --setup --local` - run e2e tests with the local version of Angular contained in the "dist" folder
* `yarn build-ie-polyfills` - generates a js file of polyfills that can be loaded in Internet Explorer.
## Using ServiceWorker locally
Since abb36e3cb, running `yarn start --prod` will no longer set up the ServiceWorker, which
Since abb36e3cb, running `yarn start -- --prod` will no longer set up the ServiceWorker, which
would require manually running `yarn sw-manifest` and `yarn sw-copy` (something that is not possible
with webpack serving the files from memory).
If you want to test ServiceWorker locally, you can use `yarn build` and serve the files in `dist/`
with `yarn http-server dist -p 4200`.
with `yarn http-server -- dist -p 4200`.
For more details see #16745.
## Guide to authoring
There are two types of content in the documentation:
There are two types of content in the documentatation:
* **API docs**: descriptions of the modules, classes, interfaces, decorators, etc that make up the Angular platform.
API docs are generated directly from the source code.
@ -111,16 +101,8 @@ yarn start
yarn docs-watch
```
>Alternatively, try the consolidated `serve-and-sync` command that builds, watches and serves in the same terminal window
```bash
yarn serve-and-sync
```
* Open a browser at https://localhost:4200/ and navigate to the document on which you want to work.
You can automatically open the browser by using `yarn start -o` in the first terminal.
You can automatically open the browser by using `yarn start -- -o` in the first terminal.
* Make changes to the page's associated doc or example files. Every time a file is saved, the doc will
be regenerated, the app will rebuild and the page will reload.
* If you get a build error complaining about examples or any other odd behavior, be sure to consult
the [Authors Style Guide](https://angular.io/guide/docs-style-guide).

View File

@ -156,7 +156,7 @@ RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
# Set up the Node.js scripts
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
WORKDIR $AIO_SCRIPTS_JS_DIR/
RUN yarn install --production --freeze-lockfile
RUN yarn install --production
# Set up health check

View File

@ -6,7 +6,7 @@ server=8.8.4.4
# Listen for DHCP and DNS requests only on this address.
listen-address=127.0.0.1
# Force an IP address for these domains.
# Force an IP addres for these domains.
address=/{{$AIO_NGINX_HOSTNAME}}/127.0.0.1
address=/{{$AIO_UPLOAD_HOSTNAME}}/127.0.0.1
address=/{{$TEST_AIO_NGINX_HOSTNAME}}/127.0.0.1

View File

@ -88,21 +88,6 @@ 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,22 +18,49 @@ 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 {newPrDir: prDir} = this.getCandidatePrDirs(pr, isPublic);
const {oldPrDir: otherVisPrDir, 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(() => this.updatePrVisibility(pr, isPublic)).
then(otherVisPrDirExisted => (otherVisPrDirExisted && this.changePrVisibility(pr, isPublic)) as any).
then(() => Promise.all([this.exists(prDir), this.exists(shaDir)])).
then(([prDirExisted, shaDirExisted]) => {
if (shaDirExisted) {
const publicOrNot = isPublic ? 'public' : 'non-public';
throw new UploadError(409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
throw new UploadError(409, `Request to overwrite existing directory: ${shaDir}`);
}
dirToRemoveOnError = prDirExisted ? shaDir : prDir;
@ -57,36 +84,6 @@ 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,31 +1,17 @@
// Imports
import {GithubPullRequests} from '../common/github-pull-requests';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from '../upload-server/build-verifier';
import {UploadError} from '../upload-server/upload-error';
import * as c from './constants';
import {BUILD_VERIFICATION_STATUS, BuildVerifier} from './build-verifier';
import {UploadError} from './upload-error';
// 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 c.BV_verify_error:
case 'FAKE_VERIFICATION_ERROR':
// For e2e tests, fake a verification error.
return Promise.reject(new UploadError(403, `Error while verifying upload for PR ${expectedPr}: Test`));
case c.BV_verify_verifiedNotTrusted:
case 'FAKE_VERIFIED_NOT_TRUSTED':
// For e2e tests, fake a `verifiedNotTrusted` verification status.
return Promise.resolve(BUILD_VERIFICATION_STATUS.verifiedNotTrusted);
default:
@ -35,4 +21,4 @@ BuildVerifier.prototype.verify = (expectedPr: number, authHeader: string) => {
};
// tslint:disable-next-line: no-var-requires
require('../upload-server/index');
require('./index');

View File

@ -13,8 +13,10 @@ const AIO_REPO_SLUG = getEnvVar('AIO_REPO_SLUG');
const AIO_TRUSTED_PR_LABEL = getEnvVar('AIO_TRUSTED_PR_LABEL');
const AIO_UPLOAD_HOSTNAME = getEnvVar('AIO_UPLOAD_HOSTNAME');
const AIO_UPLOAD_PORT = +getEnvVar('AIO_UPLOAD_PORT');
const AIO_WWW_USER = getEnvVar('AIO_WWW_USER');
// Run
process.setuid(AIO_WWW_USER); // TODO(gkalpak): Find more suitable way to run as `www-data`.
_main();
// Functions

View File

@ -1,5 +1,4 @@
// Imports
import * as bodyParser from 'body-parser';
import * as express from 'express';
import * as http from 'http';
import {GithubPullRequests} from '../common/github-pull-requests';
@ -85,7 +84,6 @@ 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];
@ -98,8 +96,8 @@ class UploadServerFactory {
} else if (!archive) {
this.throwRequestError(400, `Missing or empty '${X_FILE_HEADER}' header`, req);
} else {
Promise.resolve().
then(() => buildVerifier.verify(+pr, authHeader)).
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))).
@ -107,23 +105,8 @@ class UploadServerFactory {
}
});
middleware.get(/^\/health-check\/?$/, (_req, res) => res.sendStatus(200));
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.get('*', req => this.throwRequestError(404, 'Unknown resource', req));
middleware.all('*', req => this.throwRequestError(405, 'Unsupported method', req));
middleware.use((err: any, _req: any, res: express.Response, _next: any) => this.respondWithError(res, err));
return middleware;
@ -142,10 +125,7 @@ class UploadServerFactory {
}
protected throwRequestError(status: number, error: string, req: express.Request) {
const message = `${error} in request: ${req.method} ${req.originalUrl}` +
(!req.body ? '' : ` ${JSON.stringify(req.body)}`);
throw new UploadError(status, message);
throw new UploadError(status, `${error} in request: ${req.method} ${req.originalUrl}`);
}
}

View File

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

@ -18,7 +18,7 @@ const TEST_AIO_UPLOAD_PORT = +getEnvVar('TEST_AIO_UPLOAD_PORT');
const WWW_USER = getEnvVar('AIO_WWW_USER');
// Interfaces - Types
export interface CmdResult { success: boolean; err: Error | null; stdout: string; stderr: string; }
export interface CmdResult { success: boolean; err: Error; stdout: string; stderr: string; }
export interface FileSpecs { content?: string; size?: number; }
export type CleanUpFn = () => void;
@ -143,7 +143,7 @@ class Helper {
statusText = status[1];
} else {
statusCode = status;
statusText = http.STATUS_CODES[statusCode] || 'UNKNOWN_STATUS_CODE';
statusText = http.STATUS_CODES[statusCode];
}
return (result: CmdResult) => {
@ -196,7 +196,7 @@ class Helper {
}
// Methods - Protected
protected createCleanUpFn(fn: () => void): CleanUpFn {
protected createCleanUpFn(fn: Function): CleanUpFn {
const cleanUpFn = () => {
const idx = this.cleanUpFns.indexOf(cleanUpFn);
if (idx !== -1) {

View File

@ -317,51 +317,6 @@ 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,6 +1,5 @@
// Imports
import * as path from 'path';
import * as c from './constants';
import {helper as h} from './helper';
// Tests
@ -15,14 +14,12 @@ 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(() => {
@ -32,7 +29,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
});
describe('for a new/non-existing PR', () => {
describe('for a new PR', () => {
it('should be able to upload and serve a public build', done => {
const regexPrefix9 = `^PR: uploaded\\/${pr9} \\| SHA: ${sha9} \\| File:`;
@ -57,7 +54,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
then(() => Promise.all([
getFile(pr9, sha9, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha9, 'foo/bar.js').then(h.verifyResponse(404)),
@ -77,7 +74,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
then(h.verifyResponse(403, errorRegex9)).
then(() => {
expect(h.buildExists(pr9)).toBe(false);
@ -86,18 +83,6 @@ 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);
});
});
@ -138,7 +123,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyBuild(pr9, sha0, false);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
then(() => Promise.all([
getFile(pr9, sha0, 'index.html').then(h.verifyResponse(404)),
getFile(pr9, sha0, 'foo/bar.js').then(h.verifyResponse(404)),
@ -163,7 +148,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyBuild(pr9, sha0);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_error).
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFICATION_ERROR').
then(h.verifyResponse(403, errorRegex9)).
then(() => {
expect(h.buildExists(pr9)).toBe(true);
@ -201,7 +186,7 @@ h.runForAllSupportedSchemes((scheme, port) => describe(`integration (on ${scheme
h.createDummyBuild(pr9, sha9, false);
h.createDummyArchive(pr9, sha9, archivePath);
uploadBuild(pr9, sha9, archivePath, c.BV_verify_verifiedNotTrusted).
uploadBuild(pr9, sha9, archivePath, 'FAKE_VERIFIED_NOT_TRUSTED').
then(h.verifyResponse(409)).
then(() => {
expect(h.readBuildFile(pr9, sha9, 'index.html', false)).toMatch(idxContentRegex9);
@ -210,110 +195,6 @@ 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,7 +1,6 @@
// Imports
import * as fs from 'fs';
import * as path from 'path';
import * as c from './constants';
import {CmdResult, helper as h} from './helper';
// Tests
@ -26,13 +25,13 @@ describe('upload-server (on HTTP)', () => {
it('should disallow non-GET requests', done => {
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = /^Unknown resource/;
const bodyRegex = /^Unsupported method/;
Promise.all([
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)),
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)),
]).then(done);
});
@ -64,7 +63,7 @@ describe('upload-server (on HTTP)', () => {
it('should reject requests for which the PR verification fails', done => {
const headers = `--header "Authorization: ${c.BV_verify_error}" ${xFileHeader}`;
const headers = `--header "Authorization: FAKE_VERIFICATION_ERROR" ${xFileHeader}`;
const url = `http://${host}/create-build/${pr}/${sha9}`;
const bodyRegex = new RegExp(`Error while verifying upload for PR ${pr}: Test`);
@ -108,9 +107,8 @@ describe('upload-server (on HTTP)', () => {
[true, false].forEach(isPublic => describe(`(for ${isPublic ? 'public' : 'hidden'} builds)`, () => {
const authorizationHeader2 = isPublic ?
authorizationHeader : `--header "Authorization: ${c.BV_verify_verifiedNotTrusted}"`;
authorizationHeader : '--header "Authorization: FAKE_VERIFIED_NOT_TRUSTED"';
const cmdPrefix = curl('', `${authorizationHeader2} ${xFileHeader}`);
const overwriteRe = RegExp(`^Request to overwrite existing ${isPublic ? 'public' : 'non-public'} directory`);
it('should not overwrite existing builds', done => {
@ -121,7 +119,7 @@ describe('upload-server (on HTTP)', () => {
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9}`).
then(h.verifyResponse(409, overwriteRe)).
then(h.verifyResponse(409, /^Request to overwrite existing directory/)).
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
then(done);
});
@ -142,7 +140,7 @@ describe('upload-server (on HTTP)', () => {
expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content');
h.runCmd(`${cmdPrefix} http://${host}/create-build/${pr}/${sha9Almost}`).
then(h.verifyResponse(409, overwriteRe)).
then(h.verifyResponse(409, /^Request to overwrite existing directory/)).
then(() => expect(h.readBuildFile(pr, sha9, 'index.html', isPublic)).toBe('My content')).
then(done);
});
@ -311,7 +309,7 @@ describe('upload-server (on HTTP)', () => {
expect(h.buildExists(pr, sha0, isPublic)).toBe(false);
uploadBuild(sha0).
then(h.verifyResponse(409, overwriteRe)).
then(h.verifyResponse(409, /^Request to overwrite existing directory/)).
then(() => {
checkPrVisibility(isPublic);
expect(h.readBuildFile(pr, sha0, 'index.html', isPublic)).toContain(pr);
@ -375,194 +373,27 @@ 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 requests to unknown URLs', done => {
it('should respond with 404 for GET 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)),
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);
});
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)),
]).then(done);
});

View File

@ -8,7 +8,7 @@
"scripts": {
"prebuild": "yarn clean-dist",
"build": "tsc",
"build-watch": "yarn tsc --watch",
"build-watch": "yarn tsc -- --watch",
"clean-dist": "node --eval \"require('shelljs').rm('-rf', 'dist')\"",
"dev": "concurrently --kill-others --raw --success first \"yarn build-watch\" \"yarn test-watch\"",
"lint": "tslint --project tsconfig.json",
@ -20,26 +20,24 @@
"test-watch": "nodemon --exec \"yarn ~~test-only\" --watch dist"
},
"dependencies": {
"body-parser": "^1.18.2",
"express": "^4.15.4",
"jasmine": "^2.8.0",
"jsonwebtoken": "^8.0.1",
"shelljs": "^0.7.8",
"tslib": "^1.7.1"
"express": "^4.14.1",
"jasmine": "^2.5.3",
"jsonwebtoken": "^7.3.0",
"shelljs": "^0.7.6"
},
"devDependencies": {
"@types/body-parser": "^1.16.5",
"@types/express": "^4.0.37",
"@types/jasmine": "^2.6.0",
"@types/jsonwebtoken": "^7.2.3",
"@types/node": "^8.0.30",
"@types/shelljs": "^0.7.4",
"@types/supertest": "^2.0.3",
"concurrently": "^3.5.0",
"nodemon": "^1.12.1",
"@types/express": "^4.0.35",
"@types/jasmine": "^2.5.43",
"@types/jsonwebtoken": "^7.2.0",
"@types/node": "^7.0.5",
"@types/shelljs": "^0.7.0",
"@types/supertest": "^2.0.0",
"concurrently": "^3.3.0",
"eslint": "^3.15.0",
"eslint-plugin-jasmine": "^2.2.0",
"nodemon": "^1.11.0",
"supertest": "^3.0.0",
"tslint": "^5.7.0",
"tslint-jasmine-noSkipOrFocus": "^1.0.8",
"typescript": "^2.5.2"
"tslint": "^4.4.2",
"typescript": "^2.1.6"
}
}

View File

@ -39,8 +39,8 @@ describe('BuildCleaner', () => {
let cleanerGetExistingBuildNumbersSpy: jasmine.Spy;
let cleanerGetOpenPrNumbersSpy: jasmine.Spy;
let cleanerRemoveUnnecessaryBuildsSpy: jasmine.Spy;
let existingBuildsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void};
let openPrsDeferred: {resolve: (v?: any) => void, reject: (e?: any) => void};
let existingBuildsDeferred: {resolve: Function, reject: Function};
let openPrsDeferred: {resolve: Function, reject: Function};
let promise: Promise<void>;
beforeEach(() => {
@ -195,7 +195,7 @@ describe('BuildCleaner', () => {
describe('getOpenPrNumbers()', () => {
let prDeferred: {resolve: (v: any) => void, reject: (v: any) => void};
let prDeferred: {resolve: Function, reject: Function};
let promise: Promise<number[]>;
beforeEach(() => {
@ -277,10 +277,7 @@ describe('BuildCleaner', () => {
it('should catch errors and log them', () => {
const consoleErrorSpy = spyOn(console, 'error');
shellRmSpy.and.callFake(() => {
// tslint:disable-next-line: no-string-throw
throw 'Test';
});
shellRmSpy.and.callFake(() => { throw 'Test'; });
(cleaner as any).removeDir('/foo/bar');

View File

@ -56,7 +56,7 @@ describe('GithubApi', () => {
it('should not pass data to \'request()\'', () => {
(api.get as any)('foo', {}, {});
(api.get as Function)('foo', {}, {});
expect(apiRequestSpy).toHaveBeenCalled();
expect(apiRequestSpy.calls.argsFor(0)[2]).toBeUndefined();
@ -144,7 +144,7 @@ describe('GithubApi', () => {
describe('getPaginated()', () => {
let deferreds: {resolve: (v: any) => void, reject: (v: any) => void}[];
let deferreds: {resolve: Function, reject: Function}[];
beforeEach(() => {
deferreds = [];
@ -292,7 +292,7 @@ describe('GithubApi', () => {
describe('onResponse', () => {
let promise: Promise<object>;
let promise: Promise<Object>;
let respond: (statusCode: number) => IncomingMessage;
beforeEach(() => {

View File

@ -22,7 +22,7 @@ describe('GithubPullRequests', () => {
describe('addComment()', () => {
let prs: GithubPullRequests;
let deferred: {resolve: (v: any) => void, reject: (v: any) => void};
let deferred: {resolve: Function, reject: Function};
beforeEach(() => {
prs = new GithubPullRequests('12345', 'foo/bar');

View File

@ -43,25 +43,178 @@ describe('BuildCreator', () => {
});
describe('create()', () => {
describe('changePrVisibility()', () => {
let bcEmitSpy: jasmine.Spy;
let bcExistsSpy: jasmine.Spy;
let bcExtractArchiveSpy: jasmine.Spy;
let bcUpdatePrVisibilitySpy: jasmine.Spy;
let shellMkdirSpy: jasmine.Spy;
let shellRmSpy: 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 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;
@ -75,12 +228,20 @@ 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 => {
bcUpdatePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
bcChangePrVisibilitySpy.and.callFake(() => expect(shellMkdirSpy).not.toHaveBeenCalled());
bcExistsSpy.and.callFake((dir: string) => dir === otherVisPrDir);
bc.create(pr, sha, archive, isPublic).
then(() => {
expect(bcUpdatePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
expect(bcChangePrVisibilitySpy).toHaveBeenCalledWith(pr, isPublic);
expect(shellMkdirSpy).toHaveBeenCalled();
}).
then(done);
@ -125,6 +286,7 @@ describe('BuildCreator', () => {
beforeEach(() => {
existsValues = {
[otherVisPrDir]: false,
[prDir]: false,
[shaDir]: false,
};
@ -135,12 +297,14 @@ describe('BuildCreator', () => {
it('should abort and skip further operations if changing the PR\'s visibility fails', done => {
const mockError = new UploadError(543, 'Test');
bcUpdatePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
existsValues[otherVisPrDir] = true;
bcChangePrVisibilitySpy.and.returnValue(Promise.reject(mockError));
bc.create(pr, sha, archive, isPublic).catch(err => {
expect(err).toBe(mockError);
expect(bcExistsSpy).not.toHaveBeenCalled();
expect(bcExistsSpy).toHaveBeenCalledTimes(1);
expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
@ -153,8 +317,7 @@ describe('BuildCreator', () => {
it('should abort and skip further operations if the build does already exist', done => {
existsValues[shaDir] = true;
bc.create(pr, sha, archive, isPublic).catch(err => {
const publicOrNot = isPublic ? 'public' : 'non-public';
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
@ -164,14 +327,11 @@ describe('BuildCreator', () => {
it('should detect existing build directory after visibility change', done => {
bcUpdatePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
expect(bcExistsSpy(prDir)).toBe(false);
expect(bcExistsSpy(shaDir)).toBe(false);
existsValues[otherVisPrDir] = true;
bcChangePrVisibilitySpy.and.callFake(() => existsValues[prDir] = existsValues[shaDir] = true);
bc.create(pr, sha, archive, isPublic).catch(err => {
const publicOrNot = isPublic ? 'public' : 'non-public';
expectToBeUploadError(err, 409, `Request to overwrite existing ${publicOrNot} directory: ${shaDir}`);
expectToBeUploadError(err, 409, `Request to overwrite existing directory: ${shaDir}`);
expect(shellMkdirSpy).not.toHaveBeenCalled();
expect(bcExtractArchiveSpy).not.toHaveBeenCalled();
expect(bcEmitSpy).not.toHaveBeenCalled();
@ -223,7 +383,6 @@ describe('BuildCreator', () => {
it('should reject with an UploadError', done => {
// tslint:disable-next-line: no-string-throw
shellMkdirSpy.and.callFake(() => { throw 'Test'; });
bc.create(pr, sha, archive, isPublic).catch(err => {
expectToBeUploadError(err, 500, `Error while uploading to directory: ${shaDir}\nTest`);
@ -247,200 +406,15 @@ 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 => {
// tslint:disable-next-line: no-string-throw
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()', () => {
let fsAccessSpy: jasmine.Spy;
let fsAccessCbs: ((v?: any) => void)[];
let fsAccessCbs: Function[];
beforeEach(() => {
fsAccessCbs = [];
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: (v?: any) => void) => fsAccessCbs.push(cb));
fsAccessSpy = spyOn(fs, 'access').and.callFake((_: string, cb: Function) => fsAccessCbs.push(cb));
});
@ -484,7 +458,7 @@ describe('BuildCreator', () => {
let shellChmodSpy: jasmine.Spy;
let shellRmSpy: jasmine.Spy;
let cpExecSpy: jasmine.Spy;
let cpExecCbs: ((...args: any[]) => void)[];
let cpExecCbs: Function[];
beforeEach(() => {
cpExecCbs = [];
@ -492,7 +466,7 @@ describe('BuildCreator', () => {
consoleWarnSpy = spyOn(console, 'warn');
shellChmodSpy = spyOn(shell, 'chmod');
shellRmSpy = spyOn(shell, 'rm');
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: (...args: any[]) => void) => cpExecCbs.push(cb));
cpExecSpy = spyOn(cp, 'exec').and.callFake((_: string, cb: Function) => cpExecCbs.push(cb));
});
@ -558,11 +532,7 @@ describe('BuildCreator', () => {
done();
});
shellChmodSpy.and.callFake(() => {
// tslint:disable-next-line: no-string-throw
throw 'Test';
});
shellChmodSpy.and.callFake(() => { throw 'Test'; });
cpExecCbs[0]();
});
@ -575,11 +545,7 @@ describe('BuildCreator', () => {
done();
});
shellRmSpy.and.callFake(() => {
// tslint:disable-next-line: no-string-throw
throw 'Test';
});
shellRmSpy.and.callFake(() => { throw 'Test'; });
cpExecCbs[0]();
});

View File

@ -116,7 +116,7 @@ describe('uploadServerFactory', () => {
it('should log the server address info on \'listening\'', () => {
const consoleInfoSpy = spyOn(console, 'info');
const server = createUploadServer();
const server = createUploadServer('builds/dir');
server.address = () => ({address: 'foo', family: '', port: 1337});
expect(consoleInfoSpy).not.toHaveBeenCalled();
@ -258,12 +258,12 @@ describe('uploadServerFactory', () => {
});
it('should respond with 404 for non-GET requests', done => {
it('should respond with 405 for non-GET requests', done => {
verifyRequests([
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),
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),
], done);
});
@ -418,12 +418,12 @@ describe('uploadServerFactory', () => {
});
it('should respond with 404 for non-GET requests', done => {
it('should respond with 405 for non-GET requests', done => {
verifyRequests([
agent.put('/health-check').expect(404),
agent.post('/health-check').expect(404),
agent.patch('/health-check').expect(404),
agent.delete('/health-check').expect(404),
agent.put('/health-check').expect(405),
agent.post('/health-check').expect(405),
agent.patch('/health-check').expect(405),
agent.delete('/health-check').expect(405),
], done);
});
@ -442,141 +442,11 @@ describe('uploadServerFactory', () => {
});
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);
});
describe('GET *', () => {
it('should respond with 404', done => {
const responseBody = 'Unknown resource in request: GET /some/url';
verifyRequests([agent.get('/some/url').expect(404, responseBody)], done);
});
});
@ -584,15 +454,14 @@ describe('uploadServerFactory', () => {
describe('ALL *', () => {
it('should respond with 404', done => {
const responseFor = (method: string) => `Unknown resource in request: ${method.toUpperCase()} /some/url`;
it('should respond with 405', done => {
const responseFor = (method: string) => `Unsupported method in request: ${method.toUpperCase()} /some/url`;
verifyRequests([
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')),
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')),
], done);
});

View File

@ -1,66 +1,25 @@
{
"compilerOptions": {
/* Basic Options */
"target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
"alwaysStrict": true,
"forceConsistentCasingInFileNames": true,
"inlineSourceMap": true,
"lib": [
"es2015",
"es2016.array.include"
], /* Specify library files to be included in the compilation: */
// "allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "dist", /* Redirect output structure to the directory. */
// "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
"importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
// "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
// "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
"es2016"
],
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"outDir": "dist",
"pretty": true,
"rootDir": ".",
"skipLibCheck": true,
"strictNullChecks": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"
], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
/* Source Map Options */
// "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */
"inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
// "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Other */
"forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */
"newLine": "LF", /* Use the specified end of line sequence to be used when emitting files: "crlf" (windows) or "lf" (unix). */
"pretty": true, /* Stylize errors and messages using color and context. */
"skipLibCheck": true /* Skip type checking of all declaration files (*.d.ts). */
]
},
"include": [
"lib/**/*",

View File

@ -1,24 +1,15 @@
{
"defaultSeverity": "error",
"extends": [
"tslint:recommended"
],
"jsRules": {},
"extends": "tslint:recommended",
"rules": {
"array-type": [true, "array"],
"arrow-parens": [true, "ban-single-arg-parens"],
"interface-name": [true, "never-prefix"],
"max-classes-per-file": [true, 4],
"no-consecutive-blank-lines": [true, 2],
"no-console": [false],
"no-focused-test": true,
"no-console": false,
"no-namespace": [true, "allow-declarations"],
"no-skipped-test": true,
"no-string-literal": false,
"quotemark": [true, "single"],
"variable-name": [true, "ban-keywords", "check-format", "allow-leading-underscore"]
},
"rulesDirectory": [
"node_modules/tslint-jasmine-noSkipOrFocus"
]
}
}

File diff suppressed because it is too large Load Diff

View File

@ -6,9 +6,10 @@ export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/GITHUB_TOKEN 2>/dev/null || ec
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "MISSING_PREVIEW_DEPLOYMENT_TOKEN")
# Start the upload-server instance
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
# (Currently, there doesn't seem to be a straight forward way.)
action=$([ "$1" == "stop" ] && echo "stop" || echo "start")
pm2 $action $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server \
--uid $AIO_WWW_USER \
--log /var/log/aio/upload-server-prod.log \
--name aio-upload-server-prod \
${@:2}

View File

@ -15,12 +15,13 @@ export AIO_GITHUB_TOKEN=$(head -c -1 /aio-secrets/TEST_GITHUB_TOKEN 2>/dev/null
export AIO_PREVIEW_DEPLOYMENT_TOKEN=$(head -c -1 /aio-secrets/TEST_PREVIEW_DEPLOYMENT_TOKEN 2>/dev/null || echo "TEST_PREVIEW_DEPLOYMENT_TOKEN")
# Start the upload-server instance
# TODO(gkalpak): Ideally, the upload server should be run as a non-privileged user.
# (Currently, there doesn't seem to be a straight forward way.)
appName=aio-upload-server-test
if [[ "$1" == "stop" ]]; then
pm2 delete $appName
else
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/verify-setup/start-test-upload-server.js \
--uid $AIO_WWW_USER \
pm2 start $AIO_SCRIPTS_JS_DIR/dist/lib/upload-server/index-test.js \
--log /var/log/aio/upload-server-test.log \
--name $appName \
--no-autorestart \

View File

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

View File

@ -80,31 +80,13 @@ 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`
More info on the possible HTTP status codes and their meaning can be found
Again, more info on the possible HTTP status codes and their meaning can be found
[here](overview--http-status-codes.md).

View File

@ -42,40 +42,21 @@ 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.
- **409 (Conflict)**:
Request to overwrite existing (public or non-public) directory (e.g. deploy existing build or
change PR visibility when the destination directory does already exist).
Request to overwrite existing directory (e.g. deploy existing build or change PR visibility when
the destination directory does already exist).
- **413 (Payload Too Large)**:
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 (public or non-public) directory (i.e. directories for both
visibilities exist).
(Normally, this should not happen.)
## `https://*.ngbuilds.io/*`
- **404 (Not Found)**:

View File

@ -16,6 +16,13 @@ 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 preceding steps, we have verified that the uploaded artifacts have been uploaded by
With the preceeding 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

@ -9,7 +9,7 @@ VM host to update the preview server based on changes in the source code.
The script will pull the latest changes from the origin's master branch and examine if there have
been any changes in files inside the preview server source code directory (see below). If there are,
it will create a new image and verify that is works as expected. Finally, it will stop and remove
the old docker container and image, create a new container based on the new image and start it.
the old docker container and image, create and new container based on the new image and start it.
The script assumes that the preview server source code is in the repository's
`aio/aio-builds-setup/` directory and expects the following inputs:

View File

@ -9,7 +9,7 @@ readonly defaultImageNameAndTag="aio-builds:latest"
# (Necessary, because only `scripts-js/dist/` is copied to the docker image.)
(
cd "$SCRIPTS_JS_DIR"
yarn install --freeze-lockfile --non-interactive
yarn install
yarn build
)

View File

@ -7,6 +7,6 @@ source "`dirname $0`/_env.sh"
# Test `scripts-js/`
(
cd "$SCRIPTS_JS_DIR"
yarn install --freeze-lockfile --non-interactive
yarn install
yarn test
)

View File

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

@ -43,9 +43,13 @@ dist/
**/app/**/*.ajs.js
# aot
*/aot/**/*
!*/aot/bs-config.json
!*/aot/index.html
**/*.ngfactory.ts
**/*.ngsummary.json
**/*.ngsummary.ts
**/*.shim.ngstyle.ts
**/*.metadata.json
!aot/bs-config.json
!aot/index.html
!rollup-config.js
# i18n
@ -55,6 +59,10 @@ dist/
!testing/src/browser-test-shim.js
!testing/karma*.js
# TS to JS
!ts-to-js/js*/**/*.js
ts-to-js/js*/**/system*.js
# webpack
!webpack/**/config/*.js
!webpack/**/*webpack*.js

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

@ -335,17 +335,17 @@ describe('Animation Tests', () => {
});
}
function getBoundingClientWidth(el: ElementFinder) {
function getBoundingClientWidth(el: ElementFinder): promise.Promise<number> {
return browser.executeScript(
'return arguments[0].getBoundingClientRect().width',
el.getWebElement()
) as PromiseLike<number>;
);
}
function getOffsetWidth(el: ElementFinder) {
function getOffsetWidth(el: ElementFinder): promise.Promise<number> {
return browser.executeScript(
'return arguments[0].offsetWidth',
el.getWebElement()
) as PromiseLike<number>;
);
}
});

View File

@ -1,6 +1,5 @@
// #docplaster
import { NgModule } from '@angular/core';
// #docregion animations-module
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
// #enddocregion animations-module
@ -16,12 +15,11 @@ import { HeroListAutoComponent } from './hero-list-auto.component';
import { HeroListGroupsComponent } from './hero-list-groups.component';
import { HeroListMultistepComponent } from './hero-list-multistep.component';
import { HeroListTimingsComponent } from './hero-list-timings.component';
// #docregion animations-module
// #docregion animation-module
@NgModule({
imports: [ BrowserModule, BrowserAnimationsModule ],
// ... more stuff ...
// #enddocregion animations-module
// #enddocregion animation-module
declarations: [
HeroTeamBuilderComponent,
HeroListBasicComponent,
@ -36,8 +34,5 @@ import { HeroListTimingsComponent } from './hero-list-timings.component';
HeroListGroupsComponent
],
bootstrap: [ HeroTeamBuilderComponent ]
// #docregion animations-module
})
export class AppModule { }
// #enddocregion animations-module

View File

@ -10,7 +10,7 @@ import {
transition
} from '@angular/animations';
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-auto',
@ -43,5 +43,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListAutoComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -14,7 +14,7 @@ import {
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-basic',
@ -66,5 +66,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListBasicComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -13,7 +13,7 @@ import {
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-combined-transitions',
@ -55,5 +55,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListCombinedTransitionsComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -10,7 +10,7 @@ import {
transition
} from '@angular/animations';
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-enter-leave-states',
@ -59,5 +59,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListEnterLeaveStatesComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -10,7 +10,7 @@ import {
transition
} from '@angular/animations';
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-enter-leave',
@ -47,5 +47,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListEnterLeaveComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -11,7 +11,7 @@ import {
group
} from '@angular/animations';
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-groups',
@ -76,5 +76,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListGroupsComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -12,7 +12,7 @@ import {
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-inline-styles',
@ -56,5 +56,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListInlineStylesComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -12,7 +12,7 @@ import {
AnimationEvent
} from '@angular/animations';
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-multistep',
@ -59,7 +59,7 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListMultistepComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
animationStarted(event: AnimationEvent) {
console.warn('Animation started: ', event);

View File

@ -10,7 +10,7 @@ import {
transition
} from '@angular/animations';
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-timings',
@ -54,5 +54,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListTimingsComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -13,7 +13,7 @@ import {
} from '@angular/animations';
// #enddocregion imports
import { Hero } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-list-twoway',
@ -54,5 +54,5 @@ import { Hero } from './hero.service';
// #enddocregion animationdef
})
export class HeroListTwowayComponent {
@Input() heroes: Hero[];
@Input() heroes: Heroes;
}

View File

@ -1,41 +1,40 @@
import { Component } from '@angular/core';
import { Hero, HeroService } from './hero.service';
import { Heroes } from './hero.service';
@Component({
selector: 'hero-team-builder',
template: `
<div class="buttons">
<button [disabled]="!heroService.canAdd()" (click)="heroService.addInactive()">Add inactive hero</button>
<button [disabled]="!heroService.canAdd()" (click)="heroService.addActive()">Add active hero</button>
<button [disabled]="!heroService.canRemove()" (click)="heroService.remove()">Remove hero</button>
<button [disabled]="!heroes.canAdd()" (click)="heroes.addInactive()">Add inactive hero</button>
<button [disabled]="!heroes.canAdd()" (click)="heroes.addActive()">Add active hero</button>
<button [disabled]="!heroes.canRemove()" (click)="heroes.remove()">Remove hero</button>
</div>
<div class="columns">
<div class="column">
<h4>Basic State</h4>
<p>Switch between active/inactive on click.</p>
<hero-list-basic [heroes]="heroes"></hero-list-basic>
<hero-list-basic [heroes]=heroes></hero-list-basic>
</div>
<div class="column">
<h4>Styles inline in transitions</h4>
<p>Animated effect on click, no persistent end styles.</p>
<hero-list-inline-styles [heroes]="heroes"></hero-list-inline-styles>
<p>Animated effect on click, no persistend end styles.</p>
<hero-list-inline-styles [heroes]=heroes></hero-list-inline-styles>
</div>
<div class="column">
<h4>Combined transition syntax</h4>
<p>Switch between active/inactive on click. Define just one transition used in both directions.</p>
<hero-list-combined-transitions [heroes]="heroes"></hero-list-combined-transitions>
<hero-list-combined-transitions [heroes]=heroes></hero-list-combined-transitions>
</div>
<div class="column">
<h4>Two-way transition syntax</h4>
<p>Switch between active/inactive on click. Define just one transition used in both directions using the <=> syntax.</p>
<hero-list-twoway [heroes]="heroes"></hero-list-twoway>
<hero-list-twoway [heroes]=heroes></hero-list-twoway>
</div>
<div class="column">
<h4>Enter & Leave</h4>
<p>Enter and leave animations using the void state.</p>
<hero-list-enter-leave [heroes]="heroes"></hero-list-enter-leave>
<hero-list-enter-leave [heroes]=heroes></hero-list-enter-leave>
</div>
</div>
<div class="columns">
@ -45,27 +44,27 @@ import { Hero, HeroService } from './hero.service';
Enter and leave animations combined with active/inactive state animations.
Different enter and leave transitions depending on state.
</p>
<hero-list-enter-leave-states [heroes]="heroes"></hero-list-enter-leave-states>
<hero-list-enter-leave-states [heroes]=heroes></hero-list-enter-leave-states>
</div>
<div class="column">
<h4>Auto Style Calc</h4>
<p>Leave animation from the current computed height using the auto-style value *.</p>
<hero-list-auto [heroes]="heroes"></hero-list-auto>
<hero-list-auto [heroes]=heroes></hero-list-auto>
</div>
<div class="column">
<h4>Different Timings</h4>
<p>Enter and leave animations with different easings, ease-in for enter, ease-out for leave.</p>
<hero-list-timings [heroes]="heroes"></hero-list-timings>
<hero-list-timings [heroes]=heroes></hero-list-timings>
</div>
<div class="column">
<h4>Multiple Keyframes</h4>
<p>Enter and leave animations with three keyframes in each, to give the transition some bounce.</p>
<hero-list-multistep [heroes]="heroes"></hero-list-multistep>
<hero-list-multistep [heroes]=heroes></hero-list-multistep>
</div>
<div class="column">
<h4>Parallel Groups</h4>
<p>Enter and leave animations with multiple properties animated in parallel with different timings.</p>
<hero-list-groups [heroes]="heroes"></hero-list-groups>
<hero-list-groups [heroes]=heroes></hero-list-groups>
</div>
</div>
`,
@ -88,12 +87,8 @@ import { Hero, HeroService } from './hero.service';
min-height: 6em;
}
`],
providers: [HeroService]
providers: [Heroes]
})
export class HeroTeamBuilderComponent {
heroes: Hero[];
constructor(private heroService: HeroService) {
this.heroes = heroService.heroes;
}
constructor(private heroes: Heroes) { }
}

View File

@ -1,16 +1,16 @@
import { Injectable } from '@angular/core';
// #docregion hero
export class Hero {
constructor(public name: string, public state = 'inactive') { }
class Hero {
constructor(public name: string,
public state = 'inactive') {
}
toggleState() {
this.state = this.state === 'active' ? 'inactive' : 'active';
this.state = (this.state === 'active' ? 'inactive' : 'active');
}
}
// #enddocregion hero
const ALL_HEROES = [
let ALL_HEROES = [
'Windstorm',
'RubberMan',
'Bombasto',
@ -25,30 +25,36 @@ const ALL_HEROES = [
].map(name => new Hero(name));
@Injectable()
export class HeroService {
export class Heroes implements Iterable<Hero> {
heroes: Hero[] = [];
currentHeroes: Hero[] = [];
[Symbol.iterator]() {
return this.currentHeroes.values();
}
canAdd() {
return this.heroes.length < ALL_HEROES.length;
return this.currentHeroes.length < ALL_HEROES.length;
}
canRemove() {
return this.heroes.length > 0;
return this.currentHeroes.length > 0;
}
addActive(active = true) {
let hero = ALL_HEROES[this.heroes.length];
hero.state = active ? 'active' : 'inactive';
this.heroes.push(hero);
addActive() {
let hero = ALL_HEROES[this.currentHeroes.length];
hero.state = 'active';
this.currentHeroes.push(hero);
}
addInactive() {
this.addActive(false);
let hero = ALL_HEROES[this.currentHeroes.length];
hero.state = 'inactive';
this.currentHeroes.push(hero);
}
remove() {
this.heroes.length -= 1;
this.currentHeroes.splice(this.currentHeroes.length - 1, 1);
}
}

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 metallic objects')
new Hero('Magneta', 'Manipulates metalic 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('Welcome to My First Angular App!!');
expect(pageTitle).toEqual('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('Welcome to app!!');
expect(page.getParagraphText()).toEqual('app works!');
});
});

View File

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

View File

@ -1,20 +1,3 @@
<!--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>
<h1>
{{title}}
</h1>

View File

@ -17,16 +17,16 @@ describe('AppComponent', () => {
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
it(`should have as title 'app works!'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
expect(app.title).toEqual('app works!');
}));
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('Welcome to app!!');
expect(compiled.querySelector('h1').textContent).toContain('app works!');
}));
});

View File

@ -1,5 +1,7 @@
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';
@ -8,7 +10,9 @@ import { AppComponent } from './app.component';
AppComponent
],
imports: [
BrowserModule
BrowserModule,
FormsModule,
HttpModule
],
providers: [],
bootstrap: [AppComponent]

View File

@ -1,5 +1,5 @@
<!doctype html>
<html lang="en">
<html>
<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></app-root>
<app-root>Loading...</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';
/**
* Required to support Web Animations `@angular/animation`.
* Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation
**/
/** ALL Firefox browsers require the following to support `@angular/animation`. **/
// import 'web-animations-js'; // Run `npm install --save web-animations-js`.
@ -66,7 +66,3 @@ 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,7 +1,16 @@
{
"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,9 +1,16 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"declaration": false,
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"lib": [
"es2016"
],
"outDir": "../out-tsc/spec",
"module": "commonjs",
"target": "es5",
"target": "es6",
"baseUrl": "",
"types": [
"jasmine",
@ -14,7 +21,6 @@
"test.ts"
],
"include": [
"**/*.spec.ts",
"**/*.d.ts"
"**/*.spec.ts"
]
}

View File

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

View File

@ -2,19 +2,13 @@
"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",
"dom"
"es2016"
]
}
}

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 suppress unknown element errors
// In e2e test use CUSTOM_ELEMENTS_SCHEMA to supress unknown element errors
schemas.push(CUSTOM_ELEMENTS_SCHEMA);
}

View File

@ -12,10 +12,8 @@ describe('Component Style Tests', function () {
let componentH1 = element(by.css('hero-app > h1'));
let externalH1 = element(by.css('body > h1'));
// Note: sometimes webdriver returns the fontWeight as "normal",
// othertimes as "400", both of which are equal in CSS terms.
expect(componentH1.getCssValue('fontWeight')).toMatch(/normal|400/);
expect(externalH1.getCssValue('fontWeight')).not.toMatch(/normal|400/);
expect(componentH1.getCssValue('fontWeight')).toEqual('normal');
expect(externalH1.getCssValue('fontWeight')).not.toEqual('normal');
});

View File

@ -9,20 +9,30 @@ describe('Form Validation Tests', function () {
browser.get('');
});
describe('Template-driven form', () => {
describe('Hero Form 1', () => {
beforeAll(() => {
getPage('hero-form-template');
getPage('hero-form-template1');
});
tests('Template-Driven Form');
tests();
});
describe('Reactive form', () => {
describe('Hero Form 2', () => {
beforeAll(() => {
getPage('hero-form-reactive');
getPage('hero-form-template2');
});
tests('Reactive Form');
tests();
bobTests();
});
describe('Hero Form 3 (Reactive)', () => {
beforeAll(() => {
getPage('hero-form-reactive3');
makeNameTooLong();
});
tests();
bobTests();
});
});
@ -38,7 +48,6 @@ let page: {
nameInput: ElementFinder,
alterEgoInput: ElementFinder,
powerSelect: ElementFinder,
powerOption: ElementFinder,
errorMessages: ElementArrayFinder,
heroFormButtons: ElementArrayFinder,
heroSubmitted: ElementFinder
@ -55,21 +64,19 @@ function getPage(sectionTag: string) {
nameInput: section.element(by.css('#name')),
alterEgoInput: section.element(by.css('#alterEgo')),
powerSelect: section.element(by.css('#power')),
powerOption: section.element(by.css('#power option')),
errorMessages: section.all(by.css('div.alert')),
heroFormButtons: buttons,
heroSubmitted: section.element(by.css('.submitted-message'))
heroSubmitted: section.element(by.css('hero-submitted > div'))
};
}
function tests(title: string) {
function tests() {
it('should display correct title', function () {
expect(page.title.getText()).toContain(title);
expect(page.title.getText()).toContain('Hero Form');
});
it('should not display submitted message before submit', function () {
expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(false);
expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(false);
});
it('should have form buttons', function () {
@ -123,11 +130,11 @@ function tests(title: string) {
it('should hide form after submit', function () {
page.heroFormButtons.get(0).click();
expect(page.heroFormButtons.get(0).isDisplayed()).toBe(false);
expect(page.title.isDisplayed()).toBe(false);
});
it('submitted form should be displayed', function () {
expect(page.heroSubmitted.isElementPresent(by.css('p'))).toBe(true);
expect(page.heroSubmitted.isElementPresent(by.css('h2'))).toBe(true);
});
it('submitted form should have new hero name', function () {
@ -135,9 +142,9 @@ function tests(title: string) {
});
it('clicking edit button should reveal form again', function () {
const newFormBtn = page.heroSubmitted.element(by.css('button'));
newFormBtn.click();
expect(page.heroSubmitted.isElementPresent(by.css('p')))
const editBtn = page.heroSubmitted.element(by.css('button'));
editBtn.click();
expect(page.heroSubmitted.isElementPresent(by.css('h2')))
.toBe(false, 'submitted hidden again');
expect(page.title.isDisplayed()).toBe(true, 'can see form title');
});
@ -152,13 +159,9 @@ function expectFormIsInvalid() {
}
function bobTests() {
const emsg = 'Name cannot be Bob.';
const emsg = 'Someone named "Bob" cannot be a hero.';
it('should produce "no bob" error after setting name to "Bobby"', function () {
// Re-populate select element
page.powerSelect.click();
page.powerOption.click();
page.nameInput.clear();
page.nameInput.sendKeys('Bobby');
expectFormIsInvalid();
@ -171,3 +174,8 @@ function bobTests() {
expectFormIsValid();
});
}
function makeNameTooLong() {
// make the first name invalid
page.nameInput.sendKeys('ThisHeroNameHasWayWayTooManyLetters');
}

View File

@ -3,8 +3,10 @@ import { Component } from '@angular/core';
@Component({
selector: 'my-app',
template: `<hero-form-template></hero-form-template>
template: `<hero-form-template1></hero-form-template1>
<hr>
<hero-form-reactive></hero-form-reactive>`
<hero-form-template2></hero-form-template2>
<hr>
<hero-form-reactive3></hero-form-reactive3>`
})
export class AppComponent { }

View File

@ -1,26 +1,18 @@
// #docregion
import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { HeroFormTemplateComponent } from './template/hero-form-template.component';
import { HeroFormReactiveComponent } from './reactive/hero-form-reactive.component';
import { ForbiddenValidatorDirective } from './shared/forbidden-name.directive';
import { HeroFormTemplateModule } from './template/hero-form-template.module';
import { HeroFormReactiveModule } from './reactive/hero-form-reactive.module';
@NgModule({
imports: [
BrowserModule,
FormsModule,
ReactiveFormsModule
],
declarations: [
AppComponent,
HeroFormTemplateComponent,
HeroFormReactiveComponent,
ForbiddenValidatorDirective
HeroFormTemplateModule,
HeroFormReactiveModule
],
declarations: [ AppComponent ],
bootstrap: [ AppComponent ]
})
export class AppModule { }

View File

@ -1,38 +1,26 @@
<!-- #docregion -->
<div class="container">
<h1>Reactive Form</h1>
<form [formGroup]="heroForm" #formDir="ngForm">
<div [hidden]="formDir.submitted">
<div [hidden]="submitted">
<h1>Hero Form 3 (Reactive)</h1>
<!-- #docregion form-tag-->
<form [formGroup]="heroForm" *ngIf="active" (ngSubmit)="onSubmit()">
<!-- #enddocregion form-tag-->
<div class="form-group">
<label for="name">Name</label>
<!-- #docregion name-with-error-msg -->
<input id="name" class="form-control"
<label for="name">Name</label>
<input type="text" id="name" class="form-control"
formControlName="name" required >
<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert alert-danger">
<div *ngIf="name.errors.required">
Name is required.
</div>
<div *ngIf="name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors.forbiddenName">
Name cannot be Bob.
</div>
<div *ngIf="formErrors.name" class="alert alert-danger">
{{ formErrors.name }}
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input id="alterEgo" class="form-control"
<input type="text" id="alterEgo" class="form-control"
formControlName="alterEgo" >
</div>
@ -43,20 +31,17 @@
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="power.invalid && power.touched" class="alert alert-danger">
<div *ngIf="power.errors.required">Power is required.</div>
<div *ngIf="formErrors.power" class="alert alert-danger">
{{ formErrors.power }}
</div>
</div>
<button type="submit" class="btn btn-default"
[disabled]="heroForm.invalid">Submit</button>
[disabled]="!heroForm.valid">Submit</button>
<button type="button" class="btn btn-default"
(click)="formDir.resetForm({})">Reset</button>
</div>
</form>
<div class="submitted-message" *ngIf="formDir.submitted">
<p>You've submitted your hero, {{ heroForm.value.name }}!</p>
<button (click)="formDir.resetForm({})">Add new hero</button>
(click)="addHero()">New Hero</button>
</form>
</div>
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
</div>

View File

@ -2,39 +2,115 @@
// #docplaster
// #docregion
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { Hero } from '../shared/hero';
import { forbiddenNameValidator } from '../shared/forbidden-name.directive';
@Component({
selector: 'hero-form-reactive',
selector: 'hero-form-reactive3',
templateUrl: './hero-form-reactive.component.html'
})
export class HeroFormReactiveComponent implements OnInit {
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]};
hero = new Hero(18, 'Dr. WhatIsHisName', this.powers[0], 'Dr. What');
submitted = false;
// #docregion on-submit
onSubmit() {
this.submitted = true;
this.hero = this.heroForm.value;
}
// #enddocregion on-submit
// #enddocregion
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
active = true;
// #docregion class
// #docregion add-hero
addHero() {
this.hero = new Hero(42, '', '');
this.buildForm();
// #enddocregion add-hero
// #enddocregion class
this.active = false;
setTimeout(() => this.active = true, 0);
// #docregion
// #docregion add-hero
}
// #enddocregion add-hero
// #docregion form-builder
heroForm: FormGroup;
constructor(private fb: FormBuilder) { }
// #docregion form-group
ngOnInit(): void {
// #docregion custom-validator
this.heroForm = new FormGroup({
'name': new FormControl(this.hero.name, [
Validators.required,
Validators.minLength(4),
forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
'alterEgo': new FormControl(this.hero.alterEgo),
'power': new FormControl(this.hero.power, Validators.required)
});
// #enddocregion custom-validator
this.buildForm();
}
get name() { return this.heroForm.get('name'); }
buildForm(): void {
this.heroForm = this.fb.group({
// #docregion name-validators
'name': [this.hero.name, [
Validators.required,
Validators.minLength(4),
Validators.maxLength(24),
forbiddenNameValidator(/bob/i)
]
],
// #enddocregion name-validators
'alterEgo': [this.hero.alterEgo],
'power': [this.hero.power, Validators.required]
});
get power() { return this.heroForm.get('power'); }
// #enddocregion form-group
this.heroForm.valueChanges
.subscribe(data => this.onValueChanged(data));
this.onValueChanged(); // (re)set validation messages now
}
// #enddocregion form-builder
onValueChanged(data?: any) {
if (!this.heroForm) { return; }
const form = this.heroForm;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
formErrors = {
'name': '',
'power': ''
};
validationMessages = {
'name': {
'required': 'Name is required.',
'minlength': 'Name must be at least 4 characters long.',
'maxlength': 'Name cannot be more than 24 characters long.',
'forbiddenName': 'Someone named "Bob" cannot be a hero.'
},
'power': {
'required': 'Power is required.'
}
};
}
// #enddocregion

View File

@ -0,0 +1,13 @@
// #docregion
import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms';
import { SharedModule } from '../shared/shared.module';
import { HeroFormReactiveComponent } from './hero-form-reactive.component';
@NgModule({
imports: [ SharedModule, ReactiveFormsModule ],
declarations: [ HeroFormReactiveComponent ],
exports: [ HeroFormReactiveComponent ]
})
export class HeroFormReactiveModule { }

View File

@ -6,8 +6,9 @@ import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn, Validators } fr
/** A hero's name can't match the given regular expression */
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
const forbidden = nameRe.test(control.value);
return forbidden ? {'forbiddenName': {value: control.value}} : null;
const name = control.value;
const no = nameRe.test(name);
return no ? {'forbiddenName': {name}} : null;
};
}
// #enddocregion custom-validator
@ -19,12 +20,23 @@ export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
// #enddocregion directive-providers
})
export class ForbiddenValidatorDirective implements Validator {
export class ForbiddenValidatorDirective implements Validator, OnChanges {
@Input() forbiddenName: string;
private valFn = Validators.nullValidator;
ngOnChanges(changes: SimpleChanges): void {
const change = changes['forbiddenName'];
if (change) {
const val: string | RegExp = change.currentValue;
const re = val instanceof RegExp ? val : new RegExp(val, 'i');
this.valFn = forbiddenNameValidator(re);
} else {
this.valFn = Validators.nullValidator;
}
}
validate(control: AbstractControl): {[key: string]: any} {
return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
: null;
return this.valFn(control);
}
}
// #enddocregion directive

View File

@ -0,0 +1,9 @@
// #docregion
export class Hero {
constructor(
public id: number,
public name: string,
public power: string,
public alterEgo?: string
) { }
}

View File

@ -0,0 +1,14 @@
// #docregion
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { ForbiddenValidatorDirective } from './forbidden-name.directive';
import { SubmittedComponent } from './submitted.component';
@NgModule({
imports: [ CommonModule],
declarations: [ ForbiddenValidatorDirective, SubmittedComponent ],
exports: [ ForbiddenValidatorDirective, SubmittedComponent,
CommonModule ]
})
export class SharedModule { }

View File

@ -0,0 +1,32 @@
// #docregion
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { Hero } from './hero';
@Component({
selector: 'hero-submitted',
template: `
<div *ngIf="submitted">
<h2>You submitted the following:</h2>
<div class="row">
<div class="col-xs-3">Name</div>
<div class="col-xs-9 pull-left">{{ hero.name }}</div>
</div>
<div class="row">
<div class="col-xs-3">Alter Ego</div>
<div class="col-xs-9 pull-left">{{ hero.alterEgo }}</div>
</div>
<div class="row">
<div class="col-xs-3">Power</div>
<div class="col-xs-9 pull-left">{{ hero.power }}</div>
</div>
<br>
<button class="btn btn-default" (click)="onClick()">Edit</button>
</div>`
})
export class SubmittedComponent {
@Input() hero: Hero;
@Input() submitted = false;
@Output() submittedChange = new EventEmitter<boolean>();
onClick() { this.submittedChange.emit(false); }
}

View File

@ -1,66 +0,0 @@
<!-- #docregion -->
<div class="container">
<h1>Template-Driven Form</h1>
<!-- #docregion form-tag-->
<form #heroForm="ngForm">
<!-- #enddocregion form-tag-->
<div [hidden]="heroForm.submitted">
<div class="form-group">
<label for="name">Name</label>
<!-- #docregion name-with-error-msg -->
<!-- #docregion name-input -->
<input id="name" name="name" class="form-control"
required minlength="4" forbiddenName="bob"
[(ngModel)]="hero.name" #name="ngModel" >
<!-- #enddocregion name-input -->
<div *ngIf="name.invalid && (name.dirty || name.touched)"
class="alert alert-danger">
<div *ngIf="name.errors.required">
Name is required.
</div>
<div *ngIf="name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors.forbiddenName">
Name cannot be Bob.
</div>
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input id="alterEgo" class="form-control"
name="alterEgo" [(ngModel)]="hero.alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select id="power" name="power" class="form-control"
required [(ngModel)]="hero.power" #power="ngModel" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="power.errors && power.touched" class="alert alert-danger">
<div *ngIf="power.errors.required">Power is required.</div>
</div>
</div>
<button type="submit" class="btn btn-default"
[disabled]="heroForm.invalid">Submit</button>
<button type="button" class="btn btn-default"
(click)="heroForm.resetForm({})">Reset</button>
</div>
<div class="submitted-message" *ngIf="heroForm.submitted">
<p>You've submitted your hero, {{ heroForm.value.name }}!</p>
<button (click)="heroForm.resetForm({})">Add new hero</button>
</div>
</form>
</div>

View File

@ -1,16 +0,0 @@
/* tslint:disable: member-ordering */
// #docplaster
// #docregion
import { Component } from '@angular/core';
@Component({
selector: 'hero-form-template',
templateUrl: './hero-form-template.component.html'
})
export class HeroFormTemplateComponent {
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
hero = {name: 'Dr.', alterEgo: 'Dr. What', power: this.powers[0]};
}

View File

@ -0,0 +1,14 @@
// #docregion
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { SharedModule } from '../shared/shared.module';
import { HeroFormTemplate1Component } from './hero-form-template1.component';
import { HeroFormTemplate2Component } from './hero-form-template2.component';
@NgModule({
imports: [ SharedModule, FormsModule ],
declarations: [ HeroFormTemplate1Component, HeroFormTemplate2Component ],
exports: [ HeroFormTemplate1Component, HeroFormTemplate2Component ]
})
export class HeroFormTemplateModule { }

View File

@ -0,0 +1,61 @@
<!-- #docregion -->
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form 1 (Template)</h1>
<!-- #docregion form-tag-->
<form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()">
<!-- #enddocregion form-tag-->
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label>
<input type="text" id="name" class="form-control"
required minlength="4" maxlength="24"
name="name" [(ngModel)]="hero.name"
#name="ngModel" >
<div *ngIf="name.errors && (name.dirty || name.touched)"
class="alert alert-danger">
<div [hidden]="!name.errors.required">
Name is required
</div>
<div [hidden]="!name.errors.minlength">
Name must be at least 4 characters long.
</div>
<div [hidden]="!name.errors.maxlength">
Name cannot be more than 24 characters long.
</div>
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" id="alterEgo" class="form-control"
name="alterEgo"
[(ngModel)]="hero.alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select id="power" class="form-control"
name="power"
[(ngModel)]="hero.power" required
#power="ngModel" >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="power.errors && power.touched" class="alert alert-danger">
<div [hidden]="!power.errors.required">Power is required</div>
</div>
</div>
<button type="submit" class="btn btn-default"
[disabled]="!heroForm.form.valid">Submit</button>
<button type="button" class="btn btn-default"
(click)="addHero()">New Hero</button>
</form>
</div>
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
</div>

View File

@ -0,0 +1,47 @@
/* tslint:disable: member-ordering */
// #docplaster
// #docregion
import { Component } from '@angular/core';
import { Hero } from '../shared/hero';
@Component({
selector: 'hero-form-template1',
templateUrl: './hero-form-template1.component.html'
})
// #docregion class
export class HeroFormTemplate1Component {
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
submitted = false;
onSubmit() {
this.submitted = true;
}
// #enddocregion class
// #enddocregion
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
active = true;
// #docregion
// #docregion class
addHero() {
this.hero = new Hero(42, '', '');
// #enddocregion class
// #enddocregion
this.active = false;
setTimeout(() => this.active = true, 0);
// #docregion
// #docregion class
}
}
// #enddocregion class
// #enddocregion

View File

@ -0,0 +1,52 @@
<!-- #docregion -->
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form 2 (Template & Messages)</h1>
<!-- #docregion form-tag-->
<form #heroForm="ngForm" *ngIf="active" (ngSubmit)="onSubmit()">
<!-- #enddocregion form-tag-->
<div class="form-group">
<!-- #docregion name-with-error-msg -->
<label for="name">Name</label>
<!-- #docregion name-input -->
<input type="text" id="name" class="form-control"
required minlength="4" maxlength="24" forbiddenName="bob"
name="name" [(ngModel)]="hero.name" >
<!-- #enddocregion name-input -->
<div *ngIf="formErrors.name" class="alert alert-danger">
{{ formErrors.name }}
</div>
<!-- #enddocregion name-with-error-msg -->
</div>
<div class="form-group">
<label for="alterEgo">Alter Ego</label>
<input type="text" id="alterEgo" class="form-control"
name="alterEgo"
[(ngModel)]="hero.alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select id="power" class="form-control"
name="power"
[(ngModel)]="hero.power" required >
<option *ngFor="let p of powers" [value]="p">{{p}}</option>
</select>
<div *ngIf="formErrors.power" class="alert alert-danger">
{{ formErrors.power }}
</div>
</div>
<button type="submit" class="btn btn-default"
[disabled]="!heroForm.form.valid">Submit</button>
<button type="button" class="btn btn-default"
(click)="addHero()">New Hero</button>
</form>
</div>
<hero-submitted [hero]="hero" [(submitted)]="submitted"></hero-submitted>
</div>

View File

@ -0,0 +1,99 @@
/* tslint:disable: member-ordering forin */
// #docplaster
// #docregion
import { Component, AfterViewChecked, ViewChild } from '@angular/core';
import { NgForm } from '@angular/forms';
import { Hero } from '../shared/hero';
@Component({
selector: 'hero-form-template2',
templateUrl: './hero-form-template2.component.html'
})
export class HeroFormTemplate2Component implements AfterViewChecked {
powers = ['Really Smart', 'Super Flexible', 'Weather Changer'];
hero = new Hero(18, 'Dr. WhatIsHisWayTooLongName', this.powers[0], 'Dr. What');
submitted = false;
onSubmit() {
this.submitted = true;
}
// #enddocregion
// Reset the form with a new hero AND restore 'pristine' class state
// by toggling 'active' flag which causes the form
// to be removed/re-added in a tick via NgIf
// TODO: Workaround until NgForm has a reset method (#6822)
active = true;
// #docregion
addHero() {
this.hero = new Hero(42, '', '');
// #enddocregion
this.active = false;
setTimeout(() => this.active = true, 0);
// #docregion
}
// #docregion view-child
heroForm: NgForm;
@ViewChild('heroForm') currentForm: NgForm;
ngAfterViewChecked() {
this.formChanged();
}
formChanged() {
if (this.currentForm === this.heroForm) { return; }
this.heroForm = this.currentForm;
if (this.heroForm) {
this.heroForm.valueChanges
.subscribe(data => this.onValueChanged(data));
}
}
// #enddocregion view-child
// #docregion handler
onValueChanged(data?: any) {
if (!this.heroForm) { return; }
const form = this.heroForm.form;
for (const field in this.formErrors) {
// clear previous error message (if any)
this.formErrors[field] = '';
const control = form.get(field);
if (control && control.dirty && !control.valid) {
const messages = this.validationMessages[field];
for (const key in control.errors) {
this.formErrors[field] += messages[key] + ' ';
}
}
}
}
formErrors = {
'name': '',
'power': ''
};
// #enddocregion handler
// #docregion messages
validationMessages = {
'name': {
'required': 'Name is required.',
'minlength': 'Name must be at least 4 characters long.',
'maxlength': 'Name cannot be more than 24 characters long.',
'forbiddenName': 'Someone named "Bob" cannot be a hero.'
},
'power': {
'required': 'Power is required.'
}
};
// #enddocregion messages
}
// #enddocregion

View File

@ -1,4 +1,3 @@
.ng-valid[required], .ng-valid.required {
border-left: 5px solid #42A948; /* green */
}

View File

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

View File

@ -9,7 +9,7 @@ export class AppComponent {
wolves = 0;
gender = 'f';
fly = true;
logo = 'https://angular.io/assets/images/logos/angular/angular.png';
logo = 'https://angular.io/resources/images/logos/angular/angular.png';
count = 3;
heroes: string[] = ['Magneta', 'Celeritas', 'Dynama'];
inc(i: number) {

View File

@ -12,7 +12,7 @@ import { UserService } from './user.service';
})
export class TitleComponent {
@Input() subtitle = '';
title = 'NgModules';
title = 'Angular Modules';
// #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 original flying heroes');
expect(flyingHeroesEle.count()).toEqual(2, 'reset should restore orginal 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'})

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