Compare commits

..

72 Commits

Author SHA1 Message Date
Pete Bacon Darwin 3f79662d61 docs(CHANGELOG): update with 1.7.7 release notes 2019-02-04 13:04:28 +00:00
Pete Bacon Darwin e4830b9fd2 docs(RELEASE): store release instructions 2019-02-04 13:04:23 +00:00
Martin Staffa 5ad4f5562c fix(ngRequired): set error correctly when inside ngRepeat and false by default
Previously, in the required validator, we would read the required setting directly
from attr.required, where it is set by ngRequired.

However, when the control is inside ngRepeat, ngRequired sets it only after a another digest has
passed, which means the initial validation run of ngModel does not include the correct required
setting. (Before commit 0637a2124c this would not have been a problem,
as every observed value change triggered a validation).

We now use the initially parsed value from ngRequired in the validator.

Fixes #16814
Closes #16820
2019-01-26 11:31:56 +01:00
Martin Staffa 43bb414e23 docs(angular.copy): fix list of unsupported types 2019-01-21 20:36:49 +01:00
Martin Staffa 64c7c53190 docs(angular.merge): add notes about support and lodash compatibility
Closes #16187
Closes #14512
2019-01-21 20:36:49 +01:00
Martin Staffa 5075c870fd docs(angular.copy): list object types / features that are not supported
Closes #5085
Closes #13193
Closes #14352
Closes #15904
Closes #16055
Closes #16061
Closes #16067
2019-01-21 20:36:48 +01:00
Martin Staffa b9edb415b2 docs(angular.copy): add note about destination and source compatibility
Closes #15444
Closes #15462
2019-01-21 20:36:47 +01:00
Martin Staffa 7ca6543244 docs(ngParseExt): note limitations with ngOptions and ngRepeat
Closes #15954
Closes #15926
2019-01-21 20:36:47 +01:00
Martin Staffa e58b4ceed8 docs(input[email]): note limitations with IPv6 addresses
Closes #16599
2019-01-21 20:36:46 +01:00
Martin Staffa c97237bcb8 docs(ngDisabled): remove restriction to input element
Disabled can be set on many different elements, and might also be used
on custom controls, so it's better to remove the restriction completely

Closes #16775
2019-01-21 12:55:31 +01:00
Pete Bacon Darwin 8822a4ff69 chore(*): update CHANGELOG with release notes for 1.7.6 2019-01-17 09:20:48 +00:00
Jason Bedard 772440cdaf fix($compile): fix ng-prop-* with undefined values
Fixes #16797
Closes #16798
2019-01-11 10:04:26 -08:00
George Kalpakas 1fbddf950f chore(saucelabs): remove SL_Safari from browser list
Closes #16806
2019-01-10 21:42:12 +02:00
George Kalpakas 369ca3e13e chore(saucelabs): add SL_EDGE-1 to browser list 2019-01-10 21:42:12 +02:00
George Kalpakas 00d38e3358 chore(travis): suppress verbose log output and allow error logging
Based on angular/angular#27657.
2019-01-10 21:42:11 +02:00
George Kalpakas a0ed1b73ff chore(package): upgrade to latest karma-sauce-launcher
Based on angular/angular#27634.
2019-01-10 21:42:11 +02:00
George Kalpakas e44c3ac327 chore(package): upgrade to latest karma
This includes a Karma fix that affects CI flakiness.
Based on angular/angular#27735.
2019-01-10 21:42:11 +02:00
George Kalpakas c6551231b2 chore(saucelabs): use 'websocket' for transmission when possible 2019-01-10 21:42:10 +02:00
George Kalpakas bf073be69d chore(saucelabs): upgrade to latest SauceConnect 2019-01-10 21:42:10 +02:00
George Kalpakas aed34c75be test(ngHref): only run Edge-specific test on relevant Edge versions 2019-01-10 21:42:10 +02:00
Bernhard Kiselka 78e17f5729 docs(guide/Conceptual Overview): use exchangeratesapi.io
As fixer.io introduced an API key and thus a limitation of calls
(see https://github.com/fixerAPI/fixer#readme ),
change https://api.fixer.io to https://api.exchangeratesapi.io instead,
which is "designed and tested to handle thousands of request per second"
and has "built in Fixer.io compatibility so you can keep all the libraries you already like
and use daily" (from https://api.exchangeratesapi.io ).
The idea is from https://github.com/fixerAPI/fixer/issues/107

fixes  #16807

Closes #16808
2019-01-07 13:13:29 +02:00
Julien RAJERISON ba6f477ac8 docs(ISSUE_TEMPLATE.md): add Opera to browser list
Opera is indirectly supported, although not explicitly tested on (see
[here](https://docs.angularjs.org/misc/faq#what-browsers-does-angularjs-work-with-)
for more info), so it doesn't hurt adding it to the list of browsers.

Closes #16801
2018-12-29 12:46:36 +02:00
Frederik Prijck 6482297185 chore(package): rename to angular to match npm
Previously, angularjs was used as the name inside the package.json file.
However, angularjs is published to npm using angular.
To avoid conflicts, the name is updated to reflect the same name as being published on npm.

Fixes #16799

Closes #16800
2018-12-27 20:29:11 +02:00
Pete Bacon Darwin 41deaf1d21 test(ngAnimate): ensure that blockTransitions can be spied upon
Previously the test was assuing that this function was attached to
the window, which is not the case in production, nor in the isolated
module tests.
2018-12-12 08:10:44 +00:00
Pete Bacon Darwin 23e4138c07 chore(modules): execute modules unit tests in one karma run
SauceLabs  is struggling to keep connecting and disconnecting
for each of the modules unit test runs. This commit puts most of
the module tests into IIFEs so that they can be run in one go.

* ngMock is still tested separately since unlike the other tests
it doesn't want to have a pre-built version of ngMock available.

* ngAnimate is still tested separately because it does some funny
things with globals that were difficult to isolate in the main modules
test run.
2018-12-12 08:10:12 +00:00
Pete Bacon Darwin 0ffb30f585 chore(utils): do not mutate source arrays 2018-12-12 08:09:59 +00:00
Pete Bacon Darwin 6f52013668 test(*): isolate cache leaks from subsequent tests 2018-12-12 08:09:42 +00:00
Michał Gołębiowski-Owczarek 07e1ba29f0 test(ngSanitize): disable a failing Edge test in all versions, including 18
It's been only disabled on Edge 16/17 so far which made it fail in Edge 18.

Closes #16786
2018-12-09 03:23:44 +01:00
Pete Bacon Darwin 04c64b3d10 chore(code.angularjs.org): fix directory list paging 2018-12-06 10:34:57 +00:00
Pete Bacon Darwin 4d9a95ffdb chore(code.angularjs.org): update firebase libraries 2018-12-06 10:34:47 +00:00
Michał Gołębiowski-Owczarek 27486bd15e fix(compile): properly handle false value for boolean attrs with jQuery
jQuery skips special boolean attrs treatment in XML nodes for historical reasons
and hence AngularJS cannot freely call `.attr(attrName, false) with such
attributes. To avoid issues in XHTML, call `removeAttr` in such cases instead.

Ref jquery/jquery#4249
Fixes #16778
Closes #16779
2018-12-06 10:09:55 +01:00
Jason Bedard cf919a6fb7 fix(ngRepeat): fix reference to last collection value remaining across linkages
Ref #16776
2018-12-06 10:09:47 +01:00
Jason Bedard d4d1031bcd fix(ngRepeat): fix trackBy function being invoked with incorrect scope
Also fixes a leak of that scope across all further instances of the
repeated element.

Fixes #16776
Closes #16777
2018-12-06 10:09:38 +01:00
Joseph Jacobs 798114075f docs(tutorial/step_3): clarify how components are mapped to HTML elements
Closes #16768
2018-12-05 18:59:20 +02:00
Anthony X 838dd12ee1 docs(tutorial/step_6): fix experiment description
In the experiments section, it is suggested to add a `-` symbol to
`<option value="age">Oldest</option>`. This change is made to reverse
the sort order when selecting the `age` option.

However, this change affects the default `$ctrl.orderProp` that we had
set in `phone-list.component.js`. After making the change, our default
when refreshing the page is "Sort by: [blank]".

This commit adds some additional documentation to clarify that this
behavior makes sense and that the reader should try and fix this within
`phone-list.component.js`.

Closes #16781
2018-12-05 17:24:40 +02:00
Martin Staffa 6926223963 perf(input): prevent multiple validations on initialization
This commit updates in-built validators with observers to prevent
multiple calls to $validate that could happen on initial linking of the directives in
certain circumstances:

- when an input is wrapped in a transclude: element directive (e.g. ngRepeat),
the order of execution between ngModel and the input / validation directives changes so that
the initial observer call happens when ngModel has already been initalized,
leading to another call to $validate, which calls *all* defined validators again.
Without ngRepeat, ngModel hasn't been initialized yet, and $validate does not call the validators.

When using validators with scope expressions, the expression value is not available when
ngModel first runs the validators (e.g. ngMinlength="myMinlength"). Only in the first call to
the observer does the value become available, making a call to $validate a necessity.

This commit solves the first problem by storing the validation attribute value so we can compare
the current value and the observed value - which will be the same after compilation.

The second problem is solved by parsing the validation expression once in the link function,
so the value is available when ngModel first validates.

Closes #14691 
Closes #16760
2018-12-05 14:10:09 +01:00
Martin Staffa 5f8372ee2d chore(Saucelabs): increase max session duration
We have amassed so many e2e tests, that the default 30 minute
Saucelabs session limit is often not enough, especially during
the week, when the VMs are under heavy load.
2018-12-05 08:31:03 +01:00
Eirik Blakstad 289374a43c fix(aria/ngClick): check if element is contenteditable before blocking spacebar
`ngAria`'s `ngClick` blocks spacebar keypresses on non-blacklisted
elements, which is an issue when the element is `contenteditable`.

Closes #16762
2018-12-02 13:19:47 +02:00
Pete Bacon Darwin 3b333f3b9f chore(package): update protractor & webdriver dependencies 2018-12-02 10:06:18 +00:00
Jason Bedard 7cbb1044fc fix(input): prevent browsers from autofilling hidden inputs
Autofilling with previous values (which will then be `$interpolate`ed) could lead to XSS or errors
2018-12-02 09:48:46 +00:00
Martin Staffa a8bfeff5d8 chore(travis): put unit module tests into separate job
They take a lot of time since we created different karma jobs for them
2018-11-26 14:53:33 +01:00
Martin Staffa fc0e100c45 chore(saucelabs): always test 2 latest Safari versions
Safari 10 does not finish the tests, but Safari 11 and 12 do
2018-11-26 14:53:32 +01:00
Martin Staffa eb49f6b755 revert: fix(Angular): add workaround for Safari / Webdriver problem
This reverts commit 6b915ad9db.
Karma has this workaround built in since 3.1.0:
https://github.com/karma-runner/karma/commit/873e4f9
2018-11-26 14:53:31 +01:00
Martin Staffa f9e651d113 chore(*): update karma
This allows us to remove the workaround added in #16645
2018-11-26 14:53:23 +01:00
Jason Bedard 2f72a69ded fix($browser): normalize inputted URLs
Calls to `$browser.url` now normalize the inputted URL ensuring multiple
calls only differing in formatting do not force a browser `pushState`.

Normalization is done the same as the browser location URL and may
differ per browser and may be changed by browsers. Today no browsers
fully normalize URLs so this does not fix all instances of this issue.

See #16100
Closes #16606
2018-11-26 14:50:39 +01:00
Jason Bedard 8a67ac57fc test($browser): update MockWindow to normalize URLs similar to real window.location 2018-11-26 14:50:39 +01:00
Pete Bacon Darwin 90a41d415c fix(interpolate): do not create directives for constant media URL attributes
By creating attribute directives that watch the value of
media url attributes (e.g. `img[src]`) we caused a conflict
when both `src` and `data-src` were appearing on the
same element. As each directive was trying to write to the
attributes on the element, where AngularJS treats `src` and
`data-src` as synonymous.

This commit ensures that we do not create create such directives
when the media url attribute is a constant (no interpolation).

Because of this (and because we no longer sanitize URLs in the
`$attr.$set()` method, this commit also updates `ngHref` and
`ngSrc` to do a preliminary sanitization of URLs in case there
is no interpolation in the attribute value.

Fixes #16734
2018-11-20 14:12:22 +00:00
Volker Braun eefaa76a90 fix($q): allow third-party promise libraries
For testing, it can be useful to overwrite `$q` with other promise
implementions, such as Bluebird + angular-bluebird-promises. This broke
in v1.6.x with "TypeError: Cannot set property 'pur' of undefined".

Closes #16164

Closes #16471
2018-11-15 17:39:49 +02:00
Alejandro López bc3432365c test(e2e): replace the deprecated browser.getLocationAbsUrl() with browser.getCurrentUrl()
According to angular/protractor#3969, `browser.getLocationAbsUrl()` is
now deprecated and `browser.getCurrentUrl()` should be used instead.

Closes #16053
2018-11-15 17:14:05 +02:00
George Kalpakas f4ac5c8487 docs(tutorial): mention that Protractor might need upgrading
Since Protractor requires specific WebDriver versions and these are only
compatible with specific browser version ranges, it is often necessary
to upgrade Protractor just so that it picks up a more recent WebDriver
version.
Related: #16739

Closes #16753
2018-11-15 16:50:10 +02:00
George Kalpakas 9f2f567167 docs(tutorial): explain how to upgrade dependencies
Related: angular/angular-seed#439
2018-11-15 16:50:10 +02:00
George Kalpakas 3e6f49a227 docs(tutorial): replace getLocationAbsUrl() with getCurrentUrl()
Protractor's `browser.getLocationAbsUrl()` has been deprecated and
`browser.getCurrentUrl()` is the recommended alternative.
Related: angular/angular-phonecat#430
2018-11-15 16:50:09 +02:00
George Kalpakas 795398fab0 docs(tutorial): switch from bower to npm and upgrade AngularJS to 1.7.x
Related: angular/angular-phonecat#430
2018-11-15 16:50:08 +02:00
George Kalpakas f1a34bad20 docs(tutotial): switch all links to https 2018-11-15 16:50:07 +02:00
teresy 84a6ea9eb4 refactor(production): remove duplicate expression
Remove a duplicate expression (the left and right sides both check `versionInfo.currentVersion.version`). [48f0957](https://github.com/angular/angular.js/blob/48f0957dde728b050e2d8f76db81cbf12cffd42a/docs/config/services/deployments/production.js#L18) is the most recent commit I found where these expressions differ. My best guess is the duplicated expression can be removed.

Closes #16738
2018-11-15 16:08:12 +02:00
George Kalpakas ff0ef2aeaf test(ngOn*): add tests for binding to camelCased event names
Closes #16757
2018-11-12 13:19:32 +02:00
George Kalpakas c81e3556e8 test($compile): fix incorrect markup in tests 2018-11-12 13:19:32 +02:00
George Kalpakas 8cc69b9edb chore(i18n): fix UCD extraction for Node 10.x
Previously (e.g. Node.js 8.x), the 3rd argument to `fs.writeFile()`
(i.e. the callback) could be undefined. On Node.js 10.x, this throws an
error.

This commit fixes it by switching to `fs.writeFileSync()` which seems to
have been the original intention (based on the sorrounding code).
2018-11-12 13:19:31 +02:00
George Kalpakas ae8ce3ec60 chore(docs): fix rendering of methods' this type 2018-11-12 13:19:31 +02:00
George Kalpakas df0d5d245e chore(package): fix scripts for Node 10.x on Windows 2018-11-12 13:19:31 +02:00
Daniel Breen 67ec5e8ceb docs(guide/migration): fix typos, change 'ctrk' to 'ctrl'
Closes #16754
2018-11-02 19:35:34 +02:00
Daniel Breen 4df2188e96 docs(changelog): fix typos, change 'ctrk' to 'ctrl' 2018-11-02 19:35:34 +02:00
Hallstein Brøtan 816a587578 docs(guide/Developer Guide): correct broken link
The blog post "Creating multilingual support using AngularJS" has moved. Corrected the URL.

Closes #16746
2018-10-29 11:00:20 +02:00
Michał Gołębiowski-Owczarek 0e1bd7822e fix(urlUtils): make IPv6 URL's hostname wrapped in square brackets in IE/Edge
IE 9-11 and Edge 16-17 (fixed in 18 Preview) incorrectly don't wrap IPv6
addresses' hostnames in square brackets when parsed out of an anchor element.

Fixes #16692
Closes #16715
2018-10-18 11:45:53 +02:00
George Kalpakas b27080d525 fix(ngAnimateSwap): make it compatible with ngIf on the same element
Previously, both `ngAnimateSwap` and `ngIf` had a priority of 600, which
meant that (while both are [terminal directives][1]) they were executed
on top of each other (essentially messing each other's comment node).

This commit fixes it, by giving `ngAnimateSwap` a priority of 550, which
is lower than `ngIf` but still higher than other directives.

For reference, here is a list of built-in directive per priority:

```
-400: ngInclude, ngView
  -1: ngRef
   1: ngMessage, ngMessageDefault, ngMessageExp, ngModel, select
  10: ngModelOptions
  99: ngHref, ngSrc, ngSrcset
 100: attr interpolation, ngChecked, ngDisabled, ngList, ngMax,
      ngMaxlength, ngMin, ngMinlength, ngModel (aria), ngMultiple,
      ngOpen, ngPattern, ngProp*, ngReadonly, ngRequired, ngSelected,
      ngStep, ngValue, option
 400: ngInclude, ngView
 450: ngInit
 500: ngController
 600: ngAnimateSwap, ngIf
1000: ngNonBindable, ngRepeat
1200: ngSwitchDefault, ngSwitchWhen
```

[1]: https://docs.angularjs.org/api/ng/service/$compile#-terminal-

Fixes #16616

Closes #16729
2018-10-15 23:25:45 +03:00
George Kalpakas 1a509873bf refactor(ngAnimateSwap): remove unnecessary inject() from tests 2018-10-15 23:25:12 +03:00
Martin Staffa fead62e958 test(modules): properly isolate module tests
Closes #16712
2018-10-15 15:12:13 +02:00
Martin Staffa e2011bd0b3 docs(ngMock/ngMockE2E.$httpBackend): fix method name to matchLatestDefinitionEnabled
See #16702
2018-10-15 15:12:12 +02:00
Martin Staffa 3cdffcecba fix(ngMock): make matchLatestDefinitionEnabled work
Fixes #16702
2018-10-15 15:12:11 +02:00
Michał Gołębiowski-Owczarek f87d6648d7 chore(*): update Sauce Connect from 4.4.12 to 4.5.1
Sauce Connect 4.5.0 fixes "a major bug in connection state logic that caused
clients to exit prematurely".
See https://wiki.saucelabs.com/display/DOCS/Sauce+Connect+Proxy+Change+Logs
This will possibly improve the stability of our test runs.

Closes #16730
2018-10-15 12:59:28 +02:00
Michał Gołębiowski-Owczarek b1d0a83d01 chore(*): update minimum Yarn version, do some Yarn-related cleanups
Included changes:

*Update minimum Yarn version from 1.3.2 to 1.10.1*

Yarn 1.10 added the integrity field to the lockfile, making newer Yarn users
have their lockfile changed a lot if they run `yarn`. This commit updates the
required Yarn version to be at least 1.10.1 and changes Travis & Jenkins to use
Yarn 1.10.1

*Change the package.json's engines grunt field to grunt-cli*

The grunt field suggested it's the grunt package version we're checking while
we check the grunt-cli version instead.

*Stop separating Yarn script arguments from script names via " -- "*

The " -- " separator is necessary in npm but not in Yarn. In fact, it's
deprecated in Yarn and some future version is supposed to start passing this
parameter directly to the scripts which may break them.

*Don't install grunt-cli globally during the build*

It's enough to use `yarn grunt` instead of `grunt` and the global grunt-cli
installation is no longer needed.

*Use `yarn grunt` instead of `yarn run grunt`*

The former form is shorter.

*Don't define the `grunt` Yarn script*

As opposed to npm, `yarn binName` invokes a binary named `binName` exposed
by the respective package so the `grant` Yarn script is no longer needed.

*Allow Node versions newer than 8; bump the minimum*

Closes #16714
2018-10-11 16:36:49 +02:00
itchyny d6098eeb1c fix(ngStyle): skip setting empty value when new style has the property
Previously, all the properties in oldStyles are set to empty value once.
Using AngularJS with jQuery 3.3.1, this disables the CSS transition as
reported in jquery/jquery#4185.

Closes #16709
2018-10-04 17:03:21 +02:00
88 changed files with 7337 additions and 1506 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ https://plnkr.co or similar (you can use this template as a starting point: http
<!-- Check whether this is still an issue in the most recent stable or in the snapshot AngularJS
version (https://code.angularjs.org/snapshot/) -->
**Browser:** [all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView ]
**Browser:** [all | Chrome XX | Firefox XX | Edge XX | IE XX | Safari XX | Mobile Chrome XX | Android X.X Web Browser | iOS XX Safari | iOS XX UIWebView | iOS XX WKWebView | Opera XX ]
<!-- All browsers where this could be reproduced (and Operating System if relevant) -->
**Anything else:**
+2 -1
View File
@@ -15,6 +15,7 @@ env:
- JOB=ci-checks
- JOB=unit-core BROWSER_PROVIDER=saucelabs
- JOB=unit-jquery BROWSER_PROVIDER=saucelabs
- JOB=unit-modules BROWSER_PROVIDER=saucelabs
- JOB=docs-app BROWSER_PROVIDER=saucelabs
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs
@@ -26,7 +27,7 @@ env:
- secure: oTBjhnOKhs0qDSKTf7fE4f6DYiNDPycvB7qfSF5QRIbJK/LK/J4UtFwetXuXj79HhUZG9qnoT+5e7lPaiaMlpsIKn9ann7ffqFWN1E8TMtpJF+AGigx3djYElwfgf5nEnFUFhwjFzvbfpZNnxVGgX5YbIZpe/WUbHkP4ffU0Wks=
before_install:
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2
- curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.10.1
- export PATH="$HOME/.yarn/bin:$PATH"
before_script:
+71 -2
View File
@@ -1,3 +1,72 @@
<a name="1.7.7"></a>
# 1.7.7 kingly-exiting (2019-02-04)
## Bug Fixes
- **ngRequired:** set error correctly when inside ngRepeat and false by default
([5ad4f5](https://github.com/angular/angular.js/commit/5ad4f5562c37b1cb575e3e5fddd96e9dd10408e2),
[#16814](https://github.com/angular/angular.js/issues/16814),
[#16820](https://github.com/angular/angular.js/issues/16820))
<a name="1.7.6"></a>
# 1.7.6 gravity-manipulation (2019-01-17)
## Bug Fixes
- **$compile:** fix ng-prop-* with undefined values
([772440](https://github.com/angular/angular.js/commit/772440cdaf9a9bfa40de1675e20a5f0e356089ed),
[#16797](https://github.com/angular/angular.js/issues/16797),
[#16798](https://github.com/angular/angular.js/issues/16798))
- **compile:** properly handle false value for boolean attrs with jQuery
([27486b](https://github.com/angular/angular.js/commit/27486bd15e70946ece2ba713e4e8654b7f9bddad),
[#16778](https://github.com/angular/angular.js/issues/16778),
[#16779](https://github.com/angular/angular.js/issues/16779))
- **ngRepeat:**
- fix reference to last collection value remaining across linkages
([cf919a](https://github.com/angular/angular.js/commit/cf919a6fb7fc655f3fa37a74899a797ea5b8073e))
- fix trackBy function being invoked with incorrect scope
([d4d103](https://github.com/angular/angular.js/commit/d4d1031bcd9b30ae6a58bd60a79bcc9d20f0f2b7),
[#16776](https://github.com/angular/angular.js/issues/16776),
[#16777](https://github.com/angular/angular.js/issues/16777))
- **aria/ngClick:** check if element is `contenteditable` before blocking spacebar
([289374](https://github.com/angular/angular.js/commit/289374a43c1b2fd715ddf7455db225b17afebbaf),
[#16762](https://github.com/angular/angular.js/issues/16762))
- **input:** prevent browsers from autofilling hidden inputs
([7cbb10](https://github.com/angular/angular.js/commit/7cbb1044fcb3576cdad791bd22ebea3dfd533ff8))
- **Angular:** add workaround for Safari / Webdriver problem
([eb49f6](https://github.com/angular/angular.js/commit/eb49f6b7555cfd7ab03fd35581adb6b4bd49044e))
- **$browser:** normalize inputted URLs
([2f72a6](https://github.com/angular/angular.js/commit/2f72a69ded53a122afad3ec28d91f9bd2f41eb4f),
[#16606](https://github.com/angular/angular.js/issues/16606))
- **interpolate:** do not create directives for constant media URL attributes
([90a41d](https://github.com/angular/angular.js/commit/90a41d415c83abdbf28317f49df0fd0a7e07db86),
[#16734](https://github.com/angular/angular.js/issues/16734))
- **$q:** allow third-party promise libraries
([eefaa7](https://github.com/angular/angular.js/commit/eefaa76a90dbef08fdc7d734a205cc2de50d9f91),
[#16164](https://github.com/angular/angular.js/issues/16164),
[#16471](https://github.com/angular/angular.js/issues/16471))
- **urlUtils:** make IPv6 URL's hostname wrapped in square brackets in IE/Edge
([0e1bd7](https://github.com/angular/angular.js/commit/0e1bd7822e61822a48b8fd7ba5913a8702e6dabf),
[#16692](https://github.com/angular/angular.js/issues/16692),
[#16715](https://github.com/angular/angular.js/issues/16715))
- **ngAnimateSwap:** make it compatible with `ngIf` on the same element
([b27080](https://github.com/angular/angular.js/commit/b27080d52546409fb4e483f212f03616e2ca8037),
[#16616](https://github.com/angular/angular.js/issues/16616),
[#16729](https://github.com/angular/angular.js/issues/16729))
- **ngMock:** make matchLatestDefinitionEnabled work
([3cdffc](https://github.com/angular/angular.js/commit/3cdffcecbae71189b4db69b57fadda6608a23b61),
[#16702](https://github.com/angular/angular.js/issues/16702))
- **ngStyle:** skip setting empty value when new style has the property
([d6098e](https://github.com/angular/angular.js/commit/d6098eeb1c9510d599e9bd3cfdba7dd21e7a55a5),
[#16709](https://github.com/angular/angular.js/issues/16709))
## Performance Improvements
- **input:** prevent multiple validations on initialization
([692622](https://github.com/angular/angular.js/commit/69262239632027b373258e75c670b89132ad9edb),
[#14691](https://github.com/angular/angular.js/issues/14691),
[#16760](https://github.com/angular/angular.js/issues/16760))
<a name="1.7.5"></a>
# 1.7.5 anti-prettification (2018-10-04)
@@ -746,7 +815,7 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
link: function(scope, element, attrs, ctrl) {
var maxValidator = ctrl.$validators.max;
ctrk.$validators.max = function(modelValue, viewValue) {
ctrl.$validators.max = function(modelValue, viewValue) {
return maxValidator(modelValue, modelValue);
};
}
@@ -1579,7 +1648,7 @@ If you rely on the $modelValue validation, you can overwrite the `min`/`max` val
link: function(scope, element, attrs, ctrl) {
var maxValidator = ctrl.$validators.max;
ctrk.$validators.max = function(modelValue, viewValue) {
ctrl.$validators.max = function(modelValue, viewValue) {
return maxValidator(modelValue, modelValue);
};
}
+56 -3
View File
@@ -4,6 +4,7 @@ var serveFavicon = require('serve-favicon');
var serveStatic = require('serve-static');
var serveIndex = require('serve-index');
var files = require('./angularFiles').files;
var mergeFilesFor = require('./angularFiles').mergeFilesFor;
var util = require('./lib/grunt/utils.js');
var versionInfo = require('./lib/versions/version-info');
var path = require('path');
@@ -30,7 +31,7 @@ if (!semver.satisfies(currentYarnVersion, expectedYarnVersion)) {
}
// Grunt CLI version checks
var expectedGruntVersion = pkg.engines.grunt;
var expectedGruntVersion = pkg.engines['grunt-cli'];
var currentGruntVersions = exec('grunt --version', {silent: true}).stdout;
var match = /^grunt-cli v(.+)$/m.exec(currentGruntVersions);
if (!match) {
@@ -141,7 +142,9 @@ module.exports = function(grunt) {
'jquery-2.2': 'karma-jquery-2.2.conf.js',
'jquery-2.1': 'karma-jquery-2.1.conf.js',
docs: 'karma-docs.conf.js',
modules: 'karma-modules.conf.js'
modules: 'karma-modules.conf.js',
'modules-ngAnimate': 'karma-modules-ngAnimate.conf.js',
'modules-ngMock': 'karma-modules-ngMock.conf.js'
},
@@ -211,6 +214,12 @@ module.exports = function(grunt) {
dest: 'build/angular-touch.js',
src: util.wrap(files['angularModules']['ngTouch'], 'module')
},
touchModuleTestBundle: {
dest: 'build/test-bundles/angular-touch.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngTouch'),
suffix: 'src/module.suffix'
},
mocks: {
dest: 'build/angular-mocks.js',
src: util.wrap(files['angularModules']['ngMock'], 'module'),
@@ -220,18 +229,42 @@ module.exports = function(grunt) {
dest: 'build/angular-sanitize.js',
src: util.wrap(files['angularModules']['ngSanitize'], 'module')
},
sanitizeModuleTestBundle: {
dest: 'build/test-bundles/angular-sanitize.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngSanitize'),
suffix: 'src/module.suffix'
},
resource: {
dest: 'build/angular-resource.js',
src: util.wrap(files['angularModules']['ngResource'], 'module')
},
resourceModuleTestBundle: {
dest: 'build/test-bundles/angular-resource.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngResource'),
suffix: 'src/module.suffix'
},
messageformat: {
dest: 'build/angular-message-format.js',
src: util.wrap(files['angularModules']['ngMessageFormat'], 'module')
},
messageformatModuleTestBundle: {
dest: 'build/test-bundles/angular-message-format.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngMessageFormat'),
suffix: 'src/module.suffix'
},
messages: {
dest: 'build/angular-messages.js',
src: util.wrap(files['angularModules']['ngMessages'], 'module')
},
messagesModuleTestBundle: {
dest: 'build/test-bundles/angular-messages.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngMessages'),
suffix: 'src/module.suffix'
},
animate: {
dest: 'build/angular-animate.js',
src: util.wrap(files['angularModules']['ngAnimate'], 'module')
@@ -240,14 +273,32 @@ module.exports = function(grunt) {
dest: 'build/angular-route.js',
src: util.wrap(files['angularModules']['ngRoute'], 'module')
},
routeModuleTestBundle: {
dest: 'build/test-bundles/angular-route.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngRoute'),
suffix: 'src/module.suffix'
},
cookies: {
dest: 'build/angular-cookies.js',
src: util.wrap(files['angularModules']['ngCookies'], 'module')
},
cookiesModuleTestBundle: {
dest: 'build/test-bundles/angular-cookies.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngCookies'),
suffix: 'src/module.suffix'
},
aria: {
dest: 'build/angular-aria.js',
src: util.wrap(files['angularModules']['ngAria'], 'module')
},
ariaModuleTestBundle: {
dest: 'build/test-bundles/angular-aria.js',
prefix: 'src/module.prefix',
src: mergeFilesFor('karmaModules-ngAria'),
suffix: 'src/module.suffix'
},
parseext: {
dest: 'build/angular-parse-ext.js',
src: util.wrap(files['angularModules']['ngParseExt'], 'module')
@@ -430,7 +481,9 @@ module.exports = function(grunt) {
grunt.registerTask('test:jquery-2.1', 'Run the jQuery 2.1 unit tests with Karma', ['tests:jquery-2.1']);
grunt.registerTask('test:modules', 'Run the Karma module tests with Karma', [
'build',
'tests:modules'
'tests:modules',
'tests:modules-ngAnimate',
'tests:modules-ngMock'
]);
grunt.registerTask('test:docs', 'Run the doc-page tests with Karma', ['package', 'tests:docs']);
grunt.registerTask('test:unit', 'Run unit, jQuery and Karma module tests with Karma', [
+98
View File
@@ -0,0 +1,98 @@
# AngularJS Release instructions
## Compare the list of commits between stable and unstable
There is a script - compare-master-to-stable.js - that helps with this.
We just want to make sure that good commits (low risk fixes + docs fixes) got cherry-picked into stable branch and nothing interesting got merged only into stable branch.
## Pick a release name (for this version)
A super-heroic power (adverb-verb phrase).
## Generate release notes
Example Commit: https://github.com/angular/angular.js/commit/7ab5098c14ee4f195dbfe2681e402fe2dfeacd78
1) Run
```bash
node_modules/.bin/changez -o changes.md -v <new version> <base branch>
```
2) Review the generated file and manually fix typos, group and reorder stuff if needed.
3) Move the content into CHANGELOG.md add release code-names to headers.
4) Push the changes to your private github repo and review.
5) cherry-pick the release notes commit to the appropriate branches.
## Pick a commit to release (for this version)
Usually this will be the commit containing the release notes, but it may also be in the past.
## Run "release" script
```bash
scripts/jenkins/release.sh --git-push-dryrun=false --commit-sha=8822a4f --version-number=1.7.6 --version-name=gravity-manipulation
```
1) The SHA is of the commit to release (could be in the past).
2) The version number and code-name that should be released, not the next version number (e.g. to release 1.2.12 you enter 1.2.12 as release version and the code-name that was picked for 1.2.12, cauliflower-eradication).
3) You will need to have write access to all the AngularJS github dist repositories and publish rights for the AngularJS packages on npm.
## Update GitHub milestones
1) Create the next milestone if it doesn't exist yet-giving ita due date.
2) Move all open issues and PRs for the current milestone to the next milestone<br>
You can do this by filtering the current milestone, selecting via checklist, and moving to the next milestone within the GH issues page.
3) Close the current milestone click the milestones tab and close from there.
4) Create a new holding milestone for the release after next-but don't give it a due date otherwise that will mess up the dashboard.
## Push build artifacts to CDN
Google CDNs are fed with data from google3 every day at 11:15am PT it takes only few minutes for the import to propagate).
If we want to make our files available, we need submit our CLs before this time on the day of the release.
## Don't update the package.json (branchVersion) until the CDN has updated
This is the version used to compute what version to link to in the CDN. If you update this too early then the CDN lookup fails and you end up with 'null, for the version, which breaks the docs.
## Verify angularjs.org download modal has latest version (updates via Travis job)
The versions in the modal are updated (based on the versions available on CDN) as part of the Travis deploy stage: https://github.com/angular/angularjs.org/blob/a4d25c5abcd39e8ce19d31cb1c78073d13c4c974/.travis.yml#L26
(You may need to explicitly trigger the Travis job. e.g. re-running the last job.)
## Announce the release (via official Google accounts)
Double check that angularjs.org is up to date with the new release version before sharing.
1) Collect a list of contributors
use: `git log --format='%aN' v1.2.12..v1.2.13 | sort -u`
2) Write a blog post (for minor releases, not patch releases) and publish it with the "release" tag
3) Post on twitter as yourself (tweet from your heart; there is no template for this), retweet as @AngularJS
## Party!
## Major Release Tasks
1) Update angularjs.org to use the latest branch.
2) Write up a migration document.
3) Create a new git branch for the version that has been released (e.g. 1.8.x).
4) Check that the build and release scripts still work.
5) Update the dist-tag of the old branch, see https://github.com/angular/angular.js/pull/12722.
6) Write a blog post.
+74 -13
View File
@@ -189,21 +189,72 @@ var angularFiles = {
'src/angular.bind.js'
],
'karmaModules': [
'karmaModules-ngAnimate': [
'build/angular.js',
'@angularSrcModules',
'build/angular-mocks.js',
'test/modules/no_bootstrap.js',
'test/helpers/*.js',
'test/ngAnimate/*.js',
'test/ngMessageFormat/*.js',
'test/ngMessages/*.js',
'test/ngMock/*.js',
'test/ngCookies/*.js',
'test/ngRoute/**/*.js',
'test/ngResource/*.js',
'test/ngSanitize/**/*.js',
'test/ngTouch/**/*.js',
'test/ngAria/*.js'
'test/helpers/matchers.js',
'test/helpers/privateMocks.js',
'test/helpers/support.js',
'test/helpers/testabilityPatch.js',
'@angularSrcModuleNgAnimate',
'test/ngAnimate/**/*.js'
],
'karmaModules-ngAria': [
'@angularSrcModuleNgAria',
'test/ngAria/**/*.js'
],
'karmaModules-ngCookies': [
'@angularSrcModuleNgCookies',
'test/ngCookies/**/*.js'
],
'karmaModules-ngMessageFormat': [
'@angularSrcModuleNgMessageFormat',
'test/ngMessageFormat/**/*.js'
],
'karmaModules-ngMessages': [
'build/angular-animate.js',
'@angularSrcModuleNgMessages',
'test/ngMessages/**/*.js'
],
// ngMock doesn't include the base because it must use the ngMock src files
'karmaModules-ngMock': [
'build/angular.js',
'src/ngMock/*.js',
'test/modules/no_bootstrap.js',
'test/helpers/matchers.js',
'test/helpers/privateMocks.js',
'test/helpers/support.js',
'test/helpers/testabilityPatch.js',
'src/routeToRegExp.js',
'build/angular-animate.js',
'test/ngMock/**/*.js'
],
'karmaModules-ngResource': [
'@angularSrcModuleNgResource',
'test/ngResource/**/*.js'
],
'karmaModules-ngRoute': [
'build/angular-animate.js',
'@angularSrcModuleNgRoute',
'test/ngRoute/**/*.js'
],
'karmaModules-ngSanitize': [
'@angularSrcModuleNgSanitize',
'test/ngSanitize/**/*.js'
],
'karmaModules-ngTouch': [
'@angularSrcModuleNgTouch',
'test/ngTouch/**/*.js'
],
'karmaJquery': [
@@ -232,6 +283,16 @@ var angularFiles = {
});
});
angularFiles['angularSrcModuleNgAnimate'] = angularFiles['angularModules']['ngAnimate'];
angularFiles['angularSrcModuleNgAria'] = angularFiles['angularModules']['ngAria'];
angularFiles['angularSrcModuleNgCookies'] = angularFiles['angularModules']['ngCookies'];
angularFiles['angularSrcModuleNgMessageFormat'] = angularFiles['angularModules']['ngMessageFormat'];
angularFiles['angularSrcModuleNgMessages'] = angularFiles['angularModules']['ngMessages'];
angularFiles['angularSrcModuleNgResource'] = angularFiles['angularModules']['ngResource'];
angularFiles['angularSrcModuleNgRoute'] = angularFiles['angularModules']['ngRoute'];
angularFiles['angularSrcModuleNgSanitize'] = angularFiles['angularModules']['ngSanitize'];
angularFiles['angularSrcModuleNgTouch'] = angularFiles['angularModules']['ngTouch'];
angularFiles['angularSrcModules'] = [].concat(
angularFiles['angularModules']['ngAnimate'],
angularFiles['angularModules']['ngMessageFormat'],
@@ -15,7 +15,7 @@ var cdnUrl = googleCdnUrl + versionInfo.cdnVersion;
// docs.angularjs.org and code.angularjs.org need them.
var versionPath = versionInfo.currentVersion.isSnapshot ?
'snapshot' :
(versionInfo.currentVersion.version || versionInfo.currentVersion.version);
versionInfo.currentVersion.version;
var examplesDependencyPath = angularCodeUrl + versionPath + '/';
module.exports = function productionDeployment(getVersion) {
@@ -17,7 +17,7 @@
{% endif %}
{% if method.this %}
<h4>Method's {% code %}this{% endcode %}</h4>
<h4>Method's `this`</h4>
{$ method.this | marked $}
{% endif %}
@@ -1,4 +1,4 @@
{% if doc.this %}
<h3>Method's {% code %}this{% endcode %}</h3>
<h3>Method's `this`</h3>
{$ doc.this | marked $}
{% endif %}
{% endif %}
+3 -3
View File
@@ -186,7 +186,7 @@ Right now, the `InvoiceController` contains all logic of our example. When the a
is a good practice to move view-independent logic from the controller into a
<a name="service">{@link services service}</a>, so it can be reused by other parts
of the application as well. Later on, we could also change that service to load the exchange rates
from the web, e.g. by calling the [Fixer.io](http://fixer.io) exchange rate API, without changing the controller.
from the web, e.g. by calling the [exchangeratesapi.io](https://exchangeratesapi.io) exchange rate API, without changing the controller.
Let's refactor our example and move the currency conversion into a service in another file:
@@ -300,7 +300,7 @@ to something shorter like `a`.
## Accessing the backend
Let's finish our example by fetching the exchange rates from the [Fixer.io](http://fixer.io) exchange rate API.
Let's finish our example by fetching the exchange rates from the [exchangeratesapi.io](https://exchangeratesapi.io) exchange rate API.
The following example shows how this is done with AngularJS:
<example name="guide-concepts-3" ng-app-included="true">
@@ -331,7 +331,7 @@ The following example shows how this is done with AngularJS:
};
var refresh = function() {
var url = 'https://api.fixer.io/latest?base=USD&symbols=' + currencies.join(",");
var url = 'https://api.exchangeratesapi.io/latest?base=USD&symbols=' + currencies.join(",");
return $http.get(url).then(function(response) {
usdToForeignRates = response.data.rates;
usdToForeignRates['USD'] = 1;
+1 -1
View File
@@ -43,7 +43,7 @@ In AngularJS applications, you move the job of filling page templates with data
* **Animation:** {@link guide/animations Core concepts}, {@link ngAnimate ngAnimate API}
* **Security:** {@link guide/security Security Docs}, {@link ng.$sce Strict Contextual Escaping}, {@link ng.directive:ngCsp Content Security Policy}, {@link ngSanitize.$sanitize $sanitize}, [video](https://www.youtube.com/watch?v=18ifoT-Id54)
* **Internationalization and Localization:** {@link guide/i18n AngularJS Guide to i18n and l10n}, {@link ng.filter:date date filter}, {@link ng.filter:currency currency filter}, [Creating multilingual support](http://www.novanet.no/blog/hallstein-brotan/dates/2013/10/creating-multilingual-support-using-angularjs/)
* **Internationalization and Localization:** {@link guide/i18n AngularJS Guide to i18n and l10n}, {@link ng.filter:date date filter}, {@link ng.filter:currency currency filter}, [Creating multilingual support](https://blog.novanet.no/creating-multilingual-support-using-angularjs/)
* **Touch events:** {@link ngTouch Touch events}
* **Accessibility:** {@link guide/accessibility ngAria}
+1 -1
View File
@@ -148,7 +148,7 @@ custom directive, as seen in the following example directive definition object:
link: function(scope, element, attrs, ctrl) {
var maxValidator = ctrl.$validators.max;
ctrk.$validators.max = function(modelValue, viewValue) {
ctrl.$validators.max = function(modelValue, viewValue) {
return maxValidator(modelValue, modelValue);
};
}
+67 -23
View File
@@ -64,7 +64,7 @@ a few git commands.
### Install Git
You can download and install Git from http://git-scm.com/download. Once installed, you should have
You can download and install Git from https://git-scm.com/download. Once installed, you should have
access to the `git` command line tool. The main commands that you will need to use are:
* `git clone ...`: Clone a remote repository onto your local machine.
@@ -99,8 +99,8 @@ The tutorial instructions, from now on, assume you are running all commands from
### Install Node.js
If you want to run the preconfigured local web server and the test tools then you will also need
[Node.js v4+][node].
In order to install dependencies (such as the test tools and AngularJS itself) and run the
preconfigured local web server, you will also need [Node.js v6+][node].
You can download a Node.js installer for your operating system from https://nodejs.org/en/download/.
@@ -125,22 +125,25 @@ npm --version
[Node Version Manager (nvm)][nvm] or [Node Version Manager (nvm) for Windows][nvm-windows].
</div>
Once you have Node.js installed on your machine, you can download the tool dependencies by running:
By installing Node.js, you also get [npm][npm], which is a command line executable for downloading
and managing Node.js packages. We use it to download the AngularJS framework as well as development
and testing tools.
Once you have Node.js installed on your machine, you can download these dependencies by running:
```
npm install
```
This command reads angular-phonecat's `package.json` file and downloads the following tools into the
`node_modules` directory:
This command reads angular-phonecat's `package.json` file and downloads the following dependencies
into the `node_modules` directory:
* [Bower][bower] - client-side code package manager
* [Http-Server][http-server] - simple local static web server
* [Karma][karma] - unit test runner
* [Protractor][protractor] - end-to-end (E2E) test runner
Running `npm install` will also automatically use bower to download the AngularJS framework into the
`app/bower_components` directory.
Running `npm install` will also automatically copy the AngularJS framework and other dependencies
necessary for our app to work into the `app/lib/` directory.
<div class="alert alert-info">
Note the angular-phonecat project is setup to install and run these utilities via npm scripts.
@@ -160,23 +163,23 @@ tasks that you will need while developing:
### Install Helper Tools (optional)
The Bower, Http-Server, Karma and Protractor modules are also executables, which can be installed
globally and run directly from a terminal/command prompt. You don't need to do this to follow the
tutorial, but if you decide you do want to run them directly, you can install these modules globally
using, `sudo npm install -g ...`.
The Http-Server, Karma and Protractor modules are also executables, which can be installed globally
and run directly from a terminal/command prompt. You don't need to do this to follow the tutorial,
but if you decide you do want to run them directly, you can install these modules globally using,
`sudo npm install --global ...`.
For instance, to install the Bower command line executable you would do:
For instance, to install the `http-server` command line executable you would do:
```
sudo npm install -g bower
sudo npm install --global http-server
```
_(Omit the sudo if running on Windows)_
_(Omit the sudo if running on Windows.)_
Then you can run the bower tool directly, such as:
Then you can run the `http-server` tool directly, such as:
```
bower install
http-server ./app
```
@@ -278,6 +281,45 @@ It is good to run the E2E tests whenever you make changes to the HTML views or w
the application as a whole is executing correctly. It is very common to run E2E tests before pushing
a new commit of changes to a remote repository.
<div class="alert alert-warning">
<p>
Each version of Protractor is compatible with specific browser versions. If you are reading this
some time in the future, it is possible that the specified Protractor version is no longer
compatible with the latest version of Chrome that you are using.
</p>
<p>
If that is the case, you can try upgrading Protractor to newer version. For instructions on how
to upgrade dependencies see [Updating dependencies](tutorial/#updating-dependencies).
</p>
</div>
### Updating dependencies
In order to avoid surprises, all dependencies listed in `package.json` are pinned to specific
versions (this is what the [package-lock.json][package-lock] file is about). This ensures that the
same version of a dependency is installed every time.
Since all dependencies are acquired via npm, you can use the same tool to easily update them as
well (although you probably don't need to for the purpose of this tutorial). Simply run the
preconfigured script:
```
npm run update-deps
```
This will update all packages to the latest version that satisfy their version ranges (as specified
in `package.json`) and also copy the necessary files into `app/lib/`. For example, if `package.json`
contains `"some-package": "1.2.x"`, it will be updated to the latest 1.2.x version (e.g. 1.2.99),
but not to 1.3.x (e.g. 1.3.0).
If you want to update a dependency to a version newer than what the specificed range would permit,
you can change the version range in `package.json` and then run `npm run update-deps` as usual.
<div class="alert alert-info">
See [here][semver-ranges] for more info on the various version range formats.
</div>
### Common Issues
@@ -324,14 +366,16 @@ Now that you have set up your local machine, let's get started with the tutorial
[angular-phonecat]: https://github.com/angular/angular-phonecat
[bower]: http://bower.io/
[git]: http://git-scm.com/
[git]: https://git-scm.com/
[http-server]: https://github.com/nodeapps/http-server
[jdk]: https://en.wikipedia.org/wiki/Java_Development_Kit
[jdk-download]: http://www.oracle.com/technetwork/java/javase/downloads/index.html
[jdk-download]: https://www.oracle.com/technetwork/java/javase/downloads/index.html
[karma]: https://karma-runner.github.io/
[node]: http://nodejs.org/
[node]: https://nodejs.org/
[npm]: https://www.npmjs.com/
[nvm]: https://github.com/creationix/nvm
[nvm-windows]: https://github.com/coreybutler/nvm-windows
[package-lock]: https://docs.npmjs.com/files/package-lock.json
[protractor]: https://github.com/angular/protractor
[selenium]: http://docs.seleniumhq.org/
[selenium]: https://docs.seleniumhq.org/
[semver-ranges]: https://docs.npmjs.com/misc/semver#ranges
+7 -6
View File
@@ -51,8 +51,8 @@ The code contains some key AngularJS elements that we will need as we progress.
<head>
<meta charset="utf-8">
<title>My HTML File</title>
<link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.css" />
<script src="bower_components/angular/angular.js"></script>
<link rel="stylesheet" href="lib/bootstrap/dist/css/bootstrap.css" />
<script src="lib/angular/angular.js"></script>
</head>
<body>
@@ -84,7 +84,7 @@ For more info on `ngApp`, check out the {@link ngApp API Reference}.
**`angular.js` script tag:**
```html
<script src="bower_components/angular/angular.js"></script>
<script src="lib/angular/angular.js"></script>
```
This code downloads the `angular.js` script which registers a callback that will be executed by the
@@ -154,8 +154,8 @@ and one static binding, and our model is empty. That will soon change!
Most of the files in your working directory come from the [angular-seed project][angular-seed],
which is typically used to bootstrap new AngularJS projects. The seed project is pre-configured to
install the AngularJS framework (via `bower` into the `app/bower_components/` directory) and tools
for developing and testing a typical web application (via `npm`).
install the AngularJS framework (via `npm` into the `app/lib/` directory) and tools for developing
and testing a typical web application (via `npm`).
For the purposes of this tutorial, we modified the angular-seed with the following changes:
@@ -163,7 +163,7 @@ For the purposes of this tutorial, we modified the angular-seed with the followi
* Removed unused dependencies.
* Added phone images to `app/img/phones/`.
* Added phone data files (JSON) to `app/phones/`.
* Added a dependency on [Bootstrap](http://getbootstrap.com) in the `bower.json` file.
* Added a dependency on [Bootstrap][bootstrap-3.3] in the `package.json` file.
## Experiments
@@ -186,3 +186,4 @@ Now let's go to {@link step_01 step 1} and add some content to the web app.
[angular-seed]: https://github.com/angular/angular-seed
[bootstrap-3.3]: https://getbootstrap.com/docs/3.3
+4 -4
View File
@@ -33,7 +33,7 @@ The view is constructed by AngularJS from this template.
<html ng-app="phonecatApp">
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="lib/angular/angular.js"></script>
<script src="app.js"></script>
</head>
<body ng-controller="PhoneListController">
@@ -317,7 +317,7 @@ by utilizing components.
<ul doc-tutorial-nav="2"></ul>
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
[jasmine-home]: http://jasmine.github.io/
[jasmine-docs]: https://jasmine.github.io/api/3.3/global
[jasmine-home]: https://jasmine.github.io/
[karma]: https://karma-runner.github.io/
[mvc-pattern]: http://en.wikipedia.org/wiki/ModelViewController
[mvc-pattern]: https://en.wikipedia.org/wiki/ModelViewController
+13 -6
View File
@@ -88,6 +88,13 @@ Let's see an example:
});
```
```html
<body>
<!-- The following line is how to use the `greetUser` component above in your html doc. -->
<greet-user></greet-user>
</body>
```
Now, every time we include `<greet-user></greet-user>` in our view, AngularJS will expand it into a
DOM sub-tree constructed using the provided `template` and managed by an instance of the specified
controller.
@@ -120,14 +127,14 @@ acquired skill.
<html ng-app="phonecatApp">
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="lib/angular/angular.js"></script>
<script src="app.js"></script>
<script src="phone-list.component.js"></script>
</head>
<body>
<!-- Use a custom component to render a list of phones -->
<phone-list></phone-list>
<phone-list></phone-list> <!-- This tells AngularJS to instantiate a `phoneList` component here. -->
</body>
</html>
@@ -148,7 +155,7 @@ angular.module('phonecatApp', []);
// Register `phoneList` component, along with its associated controller and template
angular.
module('phonecatApp').
component('phoneList', {
component('phoneList', { // This name is what AngularJS uses to match to the `<phone-list>` element.
template:
'<ul>' +
'<li ng-repeat="phone in $ctrl.phones">' +
@@ -277,7 +284,7 @@ files, so it remains easy to locate as our application grows.
[case-styles]: https://en.wikipedia.org/wiki/Letter_case#Special_case_styles
[jasmine-docs]: http://jasmine.github.io/2.4/introduction.html
[jasmine-home]: http://jasmine.github.io/
[jasmine-docs]: https://jasmine.github.io/api/3.3/global
[jasmine-home]: https://jasmine.github.io/
[karma]: https://karma-runner.github.io/
[mvc-pattern]: http://en.wikipedia.org/wiki/ModelViewController
[mvc-pattern]: https://en.wikipedia.org/wiki/ModelViewController
+2
View File
@@ -230,6 +230,8 @@ You can now rerun `npm run protractor` to see the tests run.
* Reverse the sort order by adding a `-` symbol before the sorting value:
`<option value="-age">Oldest</option>`
After making this change, you'll notice that the drop-down list has a blank option selected and does not default to age anymore.
Fix this by updating the `orderProp` value in `phone-list.component.js` to match the new value on the `<option>` element.
## Summary
+2 -2
View File
@@ -8,8 +8,8 @@
Enough of building an app with three phones in a hard-coded dataset! Let's fetch a larger dataset
from our server using one of AngularJS's built-in {@link guide/services services} called
{@link ng.$http $http}. We will use AngularJS's {@link guide/di dependency injection (DI)} to provide
the service to the `phoneList` component's controller.
{@link ng.$http $http}. We will use AngularJS's {@link guide/di dependency injection (DI)} to
provide the service to the `phoneList` component's controller.
* There is now a list of 20 phones, loaded from the server.
+6 -5
View File
@@ -47,10 +47,10 @@ URLs point to the `app/img/phones/` directory.
...
<ul class="phones">
<li ng-repeat="phone in $ctrl.phones | filter:$ctrl.query | orderBy:$ctrl.orderProp" class="thumbnail">
<a href="#/phones/{{phone.id}}" class="thumb">
<a href="#!/phones/{{phone.id}}" class="thumb">
<img ng-src="{{phone.imageUrl}}" alt="{{phone.name}}" />
</a>
<a href="#/phones/{{phone.id}}">{{phone.name}}</a>
<a href="#!/phones/{{phone.id}}">{{phone.name}}</a>
<p>{{phone.snippet}}</p>
</li>
</ul>
@@ -83,7 +83,7 @@ HTTP request to an invalid location.
query.sendKeys('nexus');
element.all(by.css('.phones li a')).first().click();
expect(browser.getLocationAbsUrl()).toBe('/phones/nexus-s');
expect(browser.getCurrentUrl()).toContain('index.html#!/phones/nexus-s');
});
...
@@ -110,8 +110,9 @@ You can now rerun `npm run protractor` to see the tests run.
## Summary
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about AngularJS
layout templates and how AngularJS makes it easy to create applications that have multiple views.
Now that you have added phone images and links, go to {@link step_09 step 9} to learn about
AngularJS layout templates and how AngularJS makes it easy to create applications that have
multiple views.
<ul doc-tutorial-nav="8"></ul>
+39 -50
View File
@@ -23,49 +23,33 @@ has multiple views by adding routing, using an AngularJS module called {@link ng
The routing functionality added in this step is provided by AngularJS in the `ngRoute` module, which
is distributed separately from the core AngularJS framework.
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
`bower.json` configuration file to include the new dependency:
Since we are using [npm][npm] to install client-side dependencies, this step updates the
`package.json` configuration file to include the new dependency:
<br />
**`bower.json`:**
**`package.json`:**
```json
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
...
"dependencies": {
"angular": "1.5.x",
"angular-mocks": "1.5.x",
"angular-route": "1.5.x",
"angular": "1.7.x",
"angular-route": "1.7.x",
"bootstrap": "3.3.x"
}
},
...
}
```
The new dependency `"angular-route": "1.5.x"` tells bower to install a version of the angular-route
module that is compatible with version 1.5.x of AngularJS. We must tell bower to download and install
The new dependency `"angular-route": "1.7.x"` tells npm to install a version of the angular-route
module that is compatible with version 1.7.x of AngularJS. We must tell npm to download and install
this dependency.
```
npm install
```
<div class="alert alert-info">
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
we have preconfigured `npm install` to run bower for us.
</div>
<div class="alert alert-warning">
**Warning:** If a new version of AngularJS has been released since you last ran `npm install`, then
you may have a problem with the `bower install` due to a conflict between the versions of
angular.js that need to be installed. If you run into this issue, simply delete your
`app/bower_components` directory and then run `npm install`.
</div>
## Multiple Views, Routing and Layout Templates
@@ -127,8 +111,8 @@ service, the `$routeProvider` exposes APIs that allow you to define routes for y
</div>
AngularJS modules solve the problem of removing global variables from the application and provide a
way of configuring the injector. As opposed to AMD or require.js modules, AngularJS modules don't try
to solve the problem of script load ordering or lazy script fetching. These goals are totally
way of configuring the injector. As opposed to AMD or require.js modules, AngularJS modules don't
try to solve the problem of script load ordering or lazy script fetching. These goals are totally
independent and both module systems can live side-by-side and fulfill their goals.
To deepen your understanding on AngularJS's DI, see [Understanding Dependency Injection][wiki-di].
@@ -146,8 +130,8 @@ into the layout template. This makes it a perfect fit for our `index.html` templ
```html
<head>
...
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="lib/angular/angular.js"></script>
<script src="lib/angular-route/angular-route.js"></script>
<script src="app.module.js"></script>
<script src="app.config.js"></script>
...
@@ -203,10 +187,8 @@ code, we put it into a separate file and used the `.config` suffix.
```js
angular.
module('phonecatApp').
config(['$locationProvider', '$routeProvider',
function config($locationProvider, $routeProvider) {
$locationProvider.hashPrefix('!');
config(['$routeProvider',
function config($routeProvider) {
$routeProvider.
when('/phones', {
template: '<phone-list></phone-list>'
@@ -226,18 +208,6 @@ the corresponding services. Here, we use the
{@link ngRoute.$routeProvider#otherwise $routeProvider.otherwise()} methods to define our
application routes.
<div class="alert alert-success">
<p>
We also used {@link $locationProvider#hashPrefix $locationProvider.hashPrefix()} to set the
hash-prefix to `!`. This prefix will appear in the links to our client-side routes, right after
the hash (`#`) symbol and before the actual path (e.g. `index.html#!/some/path`).
</p>
<p>
Setting a prefix is not necessary, but it is considered a good practice (for reasons that are
outside the scope of this tutorial). `!` is the most commonly used prefix.
</p>
</div>
Our routes are defined as follows:
* `when('/phones')`: Determines the view that will be shown, when the URL hash fragment is
@@ -261,6 +231,25 @@ the route declaration — `'/phones/:phoneId'` — as a template that is matched
URL. All variables defined with the `:` prefix are extracted into the (injectable)
{@link ngRoute.$routeParams $routeParams} object.
<div class="alert alert-info">
<p>
You may have noticed, that &mdash; while the configured route paths start with `/` (e.g.
`/phones`) &mdash; the URLs used in templates start with `#!/` (e.g. `#!/phones`).
</p>
<p>
Without getting into much detail, AngularJS (by default) uses the hash part of the URL (i.e.
what comes after the hash (`#`) symbol) to determine the current route. In addition to that, you
can also specify a {@link $locationProvider#hashPrefix hash-prefix} (`!` by default) that needs
to appear after the hash symbol in order for AngularJS to consider the value an "AngularJS path"
and process it (for example, try to match it to a route).
</p>
<p>
You can find out more about how all this works in the [Using $location](guide/$location) section
of the Developer Guide. But all you need to know for now, is that the URLs to our various routes
should be prefixed with `#!`.
</p>
</div>
## The `phoneDetail` Component
@@ -345,8 +334,8 @@ any modification.
```js
files: [
'bower_components/angular/angular.js',
'bower_components/angular-route/angular-route.js',
'lib/angular/angular.js',
'lib/angular-route/angular-route.js',
...
],
```
@@ -363,7 +352,7 @@ various URLs and verifying that the correct view was rendered.
it('should redirect `index.html` to `index.html#!/phones', function() {
browser.get('index.html');
expect(browser.getLocationAbsUrl()).toBe('/phones');
expect(browser.getCurrentUrl()).toContain('index.html#!/phones');
});
...
@@ -424,6 +413,6 @@ With the routing set up and the phone list view implemented, we are ready to go
<ul doc-tutorial-nav="9"></ul>
[bower]: http://bower.io
[deep-linking]: https://en.wikipedia.org/wiki/Deep_linking
[npm]: https://www.npmjs.com/
[wiki-di]: https://github.com/angular/angular.js/wiki/Understanding-Dependency-Injection
+20 -35
View File
@@ -21,50 +21,34 @@ In this step, we will change the way our application fetches data.
The RESTful functionality is provided by AngularJS in the {@link ngResource ngResource} module, which
is distributed separately from the core AngularJS framework.
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
`bower.json` configuration file to include the new dependency:
Since we are using [npm][npm] to install client-side dependencies, this step updates the
`package.json` configuration file to include the new dependency:
<br />
**`bower.json`:**
**`package.json`:**
```
```json
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
...
"dependencies": {
"angular": "1.5.x",
"angular-mocks": "1.5.x",
"angular-resource": "1.5.x",
"angular-route": "1.5.x",
"angular": "1.7.x",
"angular-resource": "1.7.x",
"angular-route": "1.7.x",
"bootstrap": "3.3.x"
}
},
...
}
```
The new dependency `"angular-resource": "1.5.x"` tells bower to install a version of the
angular-resource module that is compatible with version 1.5.x of AngularJS. We must tell bower to
The new dependency `"angular-resource": "1.7.x"` tells npm to install a version of the
angular-resource module that is compatible with version 1.7.x of AngularJS. We must tell npm to
download and install this dependency.
```
npm install
```
<div class="alert alert-info">
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
we have preconfigured `npm install` to run bower for us.
</div>
<div class="alert alert-warning">
**Warning:** If a new version of AngularJS has been released since you last ran `npm install`, then
you may have a problem with the `bower install` due to a conflict between the versions of
angular.js that need to be installed. If you run into this issue, simply delete your
`app/bower_components` directory and then run `npm install`.
</div>
## Service
@@ -129,7 +113,7 @@ need to load the `angular-resource.js` file, which contains the `ngResource` mod
```html
<head>
...
<script src="bower_components/angular-resource/angular-resource.js"></script>
<script src="lib/angular-resource/angular-resource.js"></script>
...
<script src="core/phone/phone.module.js"></script>
<script src="core/phone/phone.service.js"></script>
@@ -141,9 +125,10 @@ need to load the `angular-resource.js` file, which contains the `ngResource` mod
## Component Controllers
We can now simplify our component controllers (`PhoneListController` and `PhoneDetailController`) by
factoring out the lower-level `$http` service, replacing it with the new `Phone` service. AngularJS's
`$resource` service is easier to use than `$http` for interacting with data sources exposed as
RESTful resources. It is also easier now to understand what the code in our controllers is doing.
factoring out the lower-level `$http` service, replacing it with the new `Phone` service.
AngularJS's `$resource` service is easier to use than `$http` for interacting with data sources
exposed as RESTful resources. It is also easier now to understand what the code in our controllers
is doing.
<br />
**`app/phone-list/phone-list.module.js`:**
@@ -240,8 +225,8 @@ Karma configuration file with angular-resource.
```js
files: [
'bower_components/angular/angular.js',
'bower_components/angular-resource/angular-resource.js',
'lib/angular/angular.js',
'lib/angular-resource/angular-resource.js',
...
],
```
@@ -319,6 +304,6 @@ Now that we have seen how to build a custom service as a RESTful client, we are
<ul doc-tutorial-nav="13"></ul>
[bower]: http://bower.io/
[jasmine-equality]: https://jasmine.github.io/2.4/custom_equality.html
[npm]: https://www.npmjs.com/
[restful]: https://en.wikipedia.org/wiki/Representational_State_Transfer
+27 -43
View File
@@ -22,59 +22,43 @@ the template code we created earlier.
## Dependencies
The animation functionality is provided by AngularJS in the `ngAnimate` module, which is distributed
separately from the core AngularJS framework. In addition we will use [jQuery][jquery] in this project
to do extra JavaScript animations.
separately from the core AngularJS framework. In addition we will use [jQuery][jquery] in this
project to do extra JavaScript animations.
Since we are using [Bower][bower] to install client-side dependencies, this step updates the
`bower.json` configuration file to include the new dependencies:
Since we are using [npm][npm] to install client-side dependencies, this step updates the
`package.json` configuration file to include the new dependencies:
<br />
**`bower.json`:**
**`package.json`:**
```
```json
{
"name": "angular-phonecat",
"description": "A starter project for AngularJS",
"version": "0.0.0",
"homepage": "https://github.com/angular/angular-phonecat",
"license": "MIT",
"private": true,
...
"dependencies": {
"angular": "1.5.x",
"angular-animate": "1.5.x",
"angular-mocks": "1.5.x",
"angular-resource": "1.5.x",
"angular-route": "1.5.x",
"angular": "1.7.x",
"angular-animate": "1.7.x",
"angular-resource": "1.7.x",
"angular-route": "1.7.x",
"bootstrap": "3.3.x",
"jquery": "3.2.x"
}
"jquery": "3.3.x"
},
...
}
```
* `"angular-animate": "1.5.x"` tells bower to install a version of the angular-animate module that
is compatible with version 1.5.x of AngularJS.
* `"jquery": "3.2.x"` tells bower to install the latest patch release of the 3.2 version of jQuery.
Note that this is not an AngularJS library; it is the standard jQuery library. We can use bower to
* `"angular-animate": "1.7.x"` tells npm to install a version of the angular-animate module that
is compatible with version 1.7.x of AngularJS.
* `"jquery": "3.3.x"` tells npm to install the latest patch release of the 3.3 version of jQuery.
Note that this is not an AngularJS library; it is the standard jQuery library. We can use npm to
install a wide range of 3rd party libraries.
Now, we must tell bower to download and install these dependencies.
Now, we must tell npm to download and install these dependencies.
```
npm install
```
<div class="alert alert-info">
**Note:** If you have bower installed globally, you can run `bower install`, but for this project
we have preconfigured `npm install` to run bower for us.
</div>
<div class="alert alert-warning">
**Warning:** If a new version of AngularJS has been released since you last ran `npm install`, then
you may have a problem with the `bower install` due to a conflict between the versions of
angular.js that need to be installed. If you run into this issue, simply delete your
`app/bower_components` directory and then run `npm install`.
</div>
## How Animations work with `ngAnimate`
@@ -101,12 +85,12 @@ code necessary to make your application "animation aware".
...
<!-- Used for JavaScript animations (include this before angular.js) -->
<script src="bower_components/jquery/dist/jquery.js"></script>
<script src="lib/jquery/dist/jquery.js"></script>
...
<!-- Adds animation support in AngularJS -->
<script src="bower_components/angular-animate/angular-animate.js"></script>
<script src="lib/angular-animate/angular-animate.js"></script>
<!-- Defines JavaScript animations -->
<script src="app.animations.js"></script>
@@ -115,8 +99,8 @@ code necessary to make your application "animation aware".
```
<div class="alert alert-error">
**Important:** Be sure to use jQuery version 2.1 or newer, when using AngularJS 1.5 or newer; jQuery 1.x is
not officially supported.
**Important:** Be sure to use jQuery version 2.1 or newer, when using AngularJS 1.5 or newer;
jQuery 1.x is not officially supported.
In order for AngularJS to detect jQuery and take advantage of it, make sure to include `jquery.js`
before `angular.js`.
</div>
@@ -556,9 +540,9 @@ There you have it! We have created a web application in a relatively short amoun
<ul doc-tutorial-nav="14"></ul>
[bower]: http://bower.io/
[caniuse-css-animation]: http://caniuse.com/#feat=css-animation
[caniuse-css-transitions]: http://caniuse.com/#feat=css-transitions
[caniuse-css-animation]: https://caniuse.com/#feat=css-animation
[caniuse-css-transitions]: https://caniuse.com/#feat=css-transitions
[jquery]: https://jquery.com/
[jquery-animate]: https://api.jquery.com/animate/
[jquery-animate]: https://api.jquery.com/animate
[mdn-animations]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Animations/Using_CSS_animations
[npm]: https://www.npmjs.com/
+1 -1
View File
@@ -22,5 +22,5 @@ If you have questions or feedback or just want to say "hi", please post a messag
[angular-seed]: https://github.com/angular/angular-seed
[gitter]: https://gitter.im/angular/angular.js
[irc]: http://webchat.freenode.net/?channels=angularjs&uio=d4
[irc]: https://webchat.freenode.net/?channels=angularjs&uio=d4
[mailing-list]: https://groups.google.com/forum/#!forum/angular
+1 -1
View File
@@ -20,7 +20,7 @@ function main() {
} catch (e) {
fs.mkdirSync(__dirname + '/../../../src/ngParseExt');
}
fs.writeFile(__dirname + '/../../../src/ngParseExt/ucd.js', code);
fs.writeFileSync(__dirname + '/../../../src/ngParseExt/ucd.js', code);
}
}
+12
View File
@@ -0,0 +1,12 @@
'use strict';
var angularFiles = require('./angularFiles');
var sharedConfig = require('./karma-shared.conf');
module.exports = function(config) {
sharedConfig(config, {testName: 'AngularJS: isolated module tests (ngAnimate)', logFile: 'karma-ngAnimate-isolated.log'});
config.set({
files: angularFiles.mergeFilesFor('karmaModules-ngAnimate')
});
};
+12
View File
@@ -0,0 +1,12 @@
'use strict';
var angularFiles = require('./angularFiles');
var sharedConfig = require('./karma-shared.conf');
module.exports = function(config) {
sharedConfig(config, {testName: 'AngularJS: isolated module tests (ngMock)', logFile: 'karma-ngMock-isolated.log'});
config.set({
files: angularFiles.mergeFilesFor('karmaModules-ngMock')
});
};
+11 -8
View File
@@ -1,17 +1,20 @@
'use strict';
var angularFiles = require('./angularFiles');
var sharedConfig = require('./karma-shared.conf');
module.exports = function(config) {
sharedConfig(config, {testName: 'AngularJS: modules', logFile: 'karma-modules.log'});
sharedConfig(config, {testName: 'AngularJS: isolated module tests', logFile: 'karma-modules-isolated.log'});
config.set({
files: angularFiles.mergeFilesFor('karmaModules'),
junitReporter: {
outputFile: 'test_out/modules.xml',
suite: 'modules'
}
files: [
'build/angular.js',
'build/angular-mocks.js',
'test/modules/no_bootstrap.js',
'test/helpers/matchers.js',
'test/helpers/privateMocks.js',
'test/helpers/support.js',
'test/helpers/testabilityPatch.js',
'build/test-bundles/angular-*.js'
]
});
};
+8 -14
View File
@@ -23,12 +23,7 @@ module.exports = function(config, specificOptions) {
// SauceLabs config for local development.
sauceLabs: {
testName: specificOptions.testName || 'AngularJS',
startConnect: true,
options: {
// We need selenium version +2.46 for Firefox 39 and the last selenium version for OS X is 2.45.
// TODO: Uncomment when there is a selenium 2.46 available for OS X.
// 'selenium-version': '2.46.0'
}
startConnect: true
},
// BrowserStack config for local development.
@@ -65,13 +60,11 @@ module.exports = function(config, specificOptions) {
'SL_Safari-1': {
base: 'SauceLabs',
browserName: 'safari',
platform: 'OS X 10.12',
version: 'latest-1'
},
'SL_Safari': {
base: 'SauceLabs',
browserName: 'safari',
platform: 'OS X 10.12',
version: 'latest'
},
'SL_IE_9': {
@@ -104,17 +97,15 @@ module.exports = function(config, specificOptions) {
platform: 'Windows 10',
version: 'latest-1'
},
'SL_iOS_10': {
'SL_iOS': {
base: 'SauceLabs',
browserName: 'iphone',
platform: 'OS X 10.12',
version: '10.3'
version: 'latest'
},
'SL_iOS_11': {
'SL_iOS-1': {
base: 'SauceLabs',
browserName: 'iphone',
platform: 'OS X 10.12',
version: '11.2'
version: 'latest-1'
},
'BS_Chrome': {
@@ -193,6 +184,9 @@ module.exports = function(config, specificOptions) {
config.sauceLabs.tunnelIdentifier = process.env.TRAVIS_JOB_NUMBER;
config.sauceLabs.recordScreenshots = true;
// Try 'websocket' for a faster transmission first. Fallback to 'polling' if necessary.
config.transports = ['websocket', 'polling'];
// Debug logging into a file, that we print out at the end of the build.
config.loggers.push({
type: 'file',
+12 -4
View File
@@ -75,10 +75,8 @@ module.exports = {
},
wrap: function(src, name) {
src.unshift('src/' + name + '.prefix');
src.push('src/' + name + '.suffix');
return src;
wrap(src, name) {
return [`src/${name}.prefix`, ...src, `src/${name}.suffix`];
},
@@ -135,6 +133,16 @@ module.exports = {
build: function(config, fn) {
var files = grunt.file.expand(config.src);
// grunt.file.expand might reorder the list of files
// when it is expanding globs, so we use prefix and suffix
// fields to ensure that files are at the start of end of
// the list (primarily for wrapping in an IIFE).
if (config.prefix) {
files = grunt.file.expand(config.prefix).concat(files);
}
if (config.suffix) {
files = files.concat(grunt.file.expand(config.suffix));
}
var styles = config.styles;
var processedStyles;
//concat
+6 -9
View File
@@ -11,14 +11,14 @@ set -e
# Curl and run this script as part of your .travis.yml before_script section:
# before_script:
# - curl https://gist.github.com/santiycr/5139565/raw/sauce_connect_setup.sh | bash
SC_VERSION="4.4.12"
SC_VERSION="4.5.2"
CONNECT_URL="https://saucelabs.com/downloads/sc-$SC_VERSION-linux.tar.gz"
CONNECT_DIR="/tmp/sauce-connect-$RANDOM"
CONNECT_DOWNLOAD="sc-$SC_VERSION-linux.tar.gz"
CONNECT_LOG="$LOGS_DIR/sauce-connect"
CONNECT_STDOUT="$LOGS_DIR/sauce-connect.stdout"
CONNECT_STDERR="$LOGS_DIR/sauce-connect.stderr"
# We don't want to create a log file because sauceconnect always logs in verbose mode. This seems
# to be overwhelming Travis and causing flakes when we are cat-ing the log in "print_logs.sh"
CONNECT_LOG="/dev/null"
# Get Connect and start it
mkdir -p $CONNECT_DIR
@@ -42,9 +42,6 @@ if [ ! -z "$BROWSER_PROVIDER_READY_FILE" ]; then
fi
echo "Starting Sauce Connect in the background, logging into:"
echo " $CONNECT_LOG"
echo " $CONNECT_STDOUT"
echo " $CONNECT_STDERR"
echo "Starting Sauce Connect in the background"
sauce-connect/bin/sc -u $SAUCE_USERNAME -k $SAUCE_ACCESS_KEY $ARGS \
--logfile $CONNECT_LOG 2> $CONNECT_STDERR 1> $CONNECT_STDOUT &
--logfile $CONNECT_LOG &
+14 -10
View File
@@ -1,5 +1,5 @@
{
"name": "angularjs",
"name": "angular",
"license": "MIT",
"branchVersion": "^1.7.0",
"branchPattern": "1.7.*",
@@ -9,15 +9,14 @@
"url": "https://github.com/angular/angular.js.git"
},
"engines": {
"node": "^8.9.1",
"yarn": ">=1.3.2",
"grunt": "^1.2.0"
"node": ">=8.12.0",
"yarn": ">=1.10.1",
"grunt-cli": "^1.2.0"
},
"scripts": {
"commit": "git-cz",
"test-i18n": "jasmine-node i18n/spec",
"test-i18n-ucd": "jasmine-node i18n/ucd/spec",
"grunt": "grunt"
"test-i18n-ucd": "jasmine-node i18n/ucd/spec"
},
"devDependencies": {
"angular-benchpress": "0.x.x",
@@ -63,7 +62,7 @@
"jquery": "3.2.1",
"jquery-2.1": "npm:jquery@2.1.4",
"jquery-2.2": "npm:jquery@2.2.4",
"karma": "^2.0.4",
"karma": "^3.1.4",
"karma-browserstack-launcher": "^1.3.0",
"karma-chrome-launcher": "^2.2.0",
"karma-edge-launcher": "^0.4.2",
@@ -72,7 +71,7 @@
"karma-jasmine": "^1.1.2",
"karma-junit-reporter": "^1.2.0",
"karma-safari-launcher": "^1.0.0",
"karma-sauce-launcher": "^1.2.0",
"karma-sauce-launcher": "^2.0.2",
"karma-script-launcher": "^1.0.0",
"karma-spec-reporter": "^0.0.32",
"load-grunt-tasks": "^3.5.0",
@@ -84,13 +83,13 @@
"npm-run": "^4.1.0",
"open-sans-fontface": "^1.4.0",
"promises-aplus-tests": "~2.1.0",
"protractor": "^5.1.2",
"protractor": "^5.4.1",
"q": "~1.0.0",
"q-io": "^1.10.9",
"qq": "^0.3.5",
"rewire": "~2.1.0",
"sax": "^1.1.1",
"selenium-webdriver": "^2.53.1",
"selenium-webdriver": "^4.0.0-alpha.1",
"semver": "^5.4.1",
"serve-favicon": "^2.3.0",
"serve-index": "^1.8.0",
@@ -100,6 +99,11 @@
"stringmap": "^0.2.2"
},
"dependencies": {},
"resolutions": {
"//1": "`natives@1.1.0` does not work with Node.js 10.x on Windows 10",
"//2": "(E.g. see https://github.com/gulpjs/gulp/issues/2162.)",
"natives": "1.1.3"
},
"commitplease": {
"style": "angular",
"nohook": true
+3 -1
View File
@@ -79,6 +79,8 @@ function capabilitiesForSauceLabs(capabilities) {
'browserName': capabilities.browserName,
'platform': capabilities.platform,
'version': capabilities.version,
'elementScrollBehavior': 1
'elementScrollBehavior': 1,
// Allow e2e test sessions to run for a maximum of 35 minutes, instead of the default 30 minutes.
'maxDuration': 2100
};
}
@@ -10,7 +10,8 @@ const BROWSER_CACHE_DURATION = 60 * 10;
const CDN_CACHE_DURATION = 60 * 60 * 12;
function sendStoredFile(request, response) {
let filePathSegments = request.path.split('/').filter((segment) => {
const requestPath = request.path || '/';
let filePathSegments = requestPath.split('/').filter((segment) => {
// Remove empty leading or trailing path parts
return segment !== '';
});
@@ -159,7 +160,11 @@ function sendStoredFile(request, response) {
const nextQuery = data[1];
const apiResponse = data[2];
if (!files.length && (!apiResponse || !apiResponse.prefixes)) {
if (
// we got no files or directories from previous query pages
!fileList.length && !directoryList.length &&
// this query page has no file or directories
!files.length && (!apiResponse || !apiResponse.prefixes)) {
return Promise.reject({
code: 404
});
@@ -190,22 +195,16 @@ const snapshotRegex = /^snapshot(-stable)?\//;
* When a new zip file is uploaded into snapshot or snapshot-stable,
* delete the previous zip file.
*/
function deleteOldSnapshotZip(event) {
const object = event.data;
function deleteOldSnapshotZip(object, context) {
const bucketId = object.bucket;
const filePath = object.name;
const contentType = object.contentType;
const resourceState = object.resourceState;
const bucket = gcs.bucket(bucketId);
const snapshotFolderMatch = filePath.match(snapshotRegex);
if (!snapshotFolderMatch ||
contentType !== 'application/zip' ||
resourceState === 'not_exists' // Deletion event
) {
if (!snapshotFolderMatch || contentType !== 'application/zip') {
return;
}
@@ -230,4 +229,4 @@ function deleteOldSnapshotZip(event) {
}
exports.sendStoredFile = functions.https.onRequest(sendStoredFile);
exports.deleteOldSnapshotZip = functions.storage.object().onChange(deleteOldSnapshotZip);
exports.deleteOldSnapshotZip = functions.storage.object().onFinalize(deleteOldSnapshotZip);
File diff suppressed because it is too large Load Diff
@@ -3,8 +3,8 @@
"description": "Cloud Functions to serve files from gcs to code.angularjs.org",
"dependencies": {
"@google-cloud/storage": "^1.1.1",
"firebase-admin": "^4.2.1",
"firebase-functions": "^0.5.9"
"firebase-admin": "^5.11.0",
"firebase-functions": "^1.0.4"
},
"private": true
}
+5 -5
View File
@@ -21,21 +21,21 @@ rm -f angular.js.size
# BUILD #
yarn run grunt -- ci-checks package --no-color
yarn grunt ci-checks package --no-color
mkdir -p test_out
# UNIT TESTS #
yarn run grunt -- test:unit --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
yarn grunt test:unit --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
# END TO END TESTS #
yarn run grunt -- test:ci-protractor
yarn grunt test:ci-protractor
# DOCS APP TESTS #
yarn run grunt -- test:docs --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
yarn grunt test:docs --browsers="$BROWSERS" --reporters=dots,junit --no-colors --no-color
# Promises/A+ TESTS #
yarn run grunt -- test:promises-aplus --no-color
yarn grunt test:promises-aplus --no-color
# CHECK SIZE #
+2 -2
View File
@@ -8,11 +8,11 @@ nvm install
# clean out and install yarn
rm -rf ~/.yarn
curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.3.2
curl -o- -L https://yarnpkg.com/install.sh | bash -s -- --version 1.10.1
export PATH="$HOME/.yarn/bin:$PATH"
# Ensure that we have the local dependencies installed
yarn install
echo testing grunt version
yarn run grunt -- --version
yarn grunt --version
+1 -1
View File
@@ -37,7 +37,7 @@ function init {
function build {
cd ../..
source scripts/jenkins/init-node.sh
yarn run grunt -- ci-checks package --no-color
yarn grunt ci-checks package --no-color
cd $SCRIPT_DIR
}
+3 -5
View File
@@ -2,8 +2,6 @@
set -e
yarn global add grunt-cli@1.2.0
mkdir -p "$LOGS_DIR"
if [ "$JOB" != "ci-checks" ]; then
@@ -13,13 +11,13 @@ fi
# ci-checks and unit tests do not run against the packaged code
if [[ "$JOB" != "ci-checks" ]] && [[ "$JOB" != unit-* ]]; then
grunt package
yarn grunt package
fi
# unit runs the docs tests too which need a built version of the code
if [[ "$JOB" = unit-* ]]; then
grunt validate-angular-files
grunt build
yarn grunt validate-angular-files
yarn grunt build
fi
# check this after the package, because at this point the browser_provider
+10 -7
View File
@@ -11,14 +11,14 @@ export SAUCE_ACCESS_KEY
BROWSER_STACK_ACCESS_KEY=$(echo "$BROWSER_STACK_ACCESS_KEY" | rev)
SAUCE_ACCESS_KEY=$(echo "$SAUCE_ACCESS_KEY" | rev)
# TODO: restore "SL_EDGE-1" once Sauce Labs adds Edge 17 and "SL_EDGE-1" refers
# to version 16. Edge 15 disconnects from Karma frequently causing extreme build instability.
# The currently latest version of Safari on Saucelabs (v12) is unstable and disconnects frequently.
# TODO: Add `SL_Safari` back, once/if it becomes more stable again.
BROWSERS="SL_Chrome,SL_Chrome-1,\
SL_Firefox,SL_Firefox-1,\
SL_Safari,SL_Safari-1,\
SL_iOS_10,SL_iOS_11,\
SL_Safari-1,\
SL_iOS,SL_iOS-1,\
SL_IE_9,SL_IE_10,SL_IE_11,\
SL_EDGE"
SL_EDGE,SL_EDGE-1"
case "$JOB" in
"ci-checks")
@@ -29,19 +29,21 @@ case "$JOB" in
# convert commit range to 2 dots, as commitplease uses `git log`.
# See https://github.com/travis-ci/travis-ci/issues/4596 for more info
echo "Validate commit messages in PR:"
yarn run commitplease -- "${TRAVIS_COMMIT_RANGE/.../..}"
yarn run commitplease "${TRAVIS_COMMIT_RANGE/.../..}"
fi
;;
"unit-core")
grunt test:promises-aplus
grunt test:jqlite --browsers="$BROWSERS" --reporters=spec
grunt test:modules --browsers="$BROWSERS" --reporters=spec
;;
"unit-jquery")
grunt test:jquery --browsers="$BROWSERS" --reporters=spec
grunt test:jquery-2.2 --browsers="$BROWSERS" --reporters=spec
grunt test:jquery-2.1 --browsers="$BROWSERS" --reporters=spec
;;
"unit-modules")
grunt test:modules --browsers="$BROWSERS" --reporters=spec
;;
"docs-app")
grunt tests:docs --browsers="$BROWSERS" --reporters=spec
grunt test:travis-protractor --specs="docs/app/e2e/**/*.scenario.js"
@@ -102,6 +104,7 @@ case "$JOB" in
'ci-checks',\
'unit-core',\
'unit-jquery',\
'unit-modules',\
'docs-app',\
'e2e',\
or\
+1 -1
View File
@@ -7,5 +7,5 @@ for FILE in $LOG_FILES; do
echo "================================================================================"
echo " $FILE"
echo "================================================================================"
cat $FILE
cat $FILE || true
done
+27 -9
View File
@@ -396,8 +396,8 @@ function extend(dst) {
* sinceVersion="1.6.5"
* This function is deprecated, but will not be removed in the 1.x lifecycle.
* There are edge cases (see {@link angular.merge#known-issues known issues}) that are not
* supported by this function. We suggest
* using [lodash's merge()](https://lodash.com/docs/4.17.4#merge) instead.
* supported by this function. We suggest using another, similar library for all-purpose merging,
* such as [lodash's merge()](https://lodash.com/docs/4.17.4#merge).
*
* @knownIssue
* This is a list of (known) object types that are not handled correctly by this function:
@@ -406,6 +406,8 @@ function extend(dst) {
* - [`CanvasGradient`](https://developer.mozilla.org/docs/Web/API/CanvasGradient)
* - AngularJS {@link $rootScope.Scope scopes};
*
* `angular.merge` also does not support merging objects with circular references.
*
* @param {Object} dst Destination object.
* @param {...Object} src Source object(s).
* @returns {Object} Reference to `dst`.
@@ -783,7 +785,9 @@ function arrayRemove(array, value) {
* @kind function
*
* @description
* Creates a deep copy of `source`, which should be an object or an array.
* Creates a deep copy of `source`, which should be an object or an array. This functions is used
* internally, mostly in the change-detection code. It is not intended as an all-purpose copy
* function, and has several limitations (see below).
*
* * If no destination is supplied, a copy of the object or array is created.
* * If a destination is provided, all of its elements (for arrays) or properties (for objects)
@@ -798,6 +802,25 @@ function arrayRemove(array, value) {
* and on `destination`) will be ignored.
* </div>
*
* <div class="alert alert-warning">
* `angular.copy` does not check if destination and source are of the same type. It's the
* developer's responsibility to make sure they are compatible.
* </div>
*
* @knownIssue
* This is a non-exhaustive list of object types / features that are not handled correctly by
* `angular.copy`. Note that since this functions is used by the change detection code, this
* means binding or watching objects of these types (or that include these types) might not work
* correctly.
* - [`File`](https://developer.mozilla.org/docs/Web/API/File)
* - [`Map`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Map)
* - [`ImageData`](https://developer.mozilla.org/docs/Web/API/ImageData)
* - [`MediaStream`](https://developer.mozilla.org/docs/Web/API/MediaStream)
* - [`Set`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Set)
* - [`WeakMap`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/WeakMap)
* - ['getter'](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)/
* [`setter`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/set)`
*
* @param {*} source The source that will be used to make a copy. Can be any type, including
* primitives, `null`, and `undefined`.
* @param {(Object|Array)=} destination Destination into which the source is copied. If provided,
@@ -1696,13 +1719,8 @@ function angularInit(element, bootstrap) {
});
if (appElement) {
if (!isAutoBootstrapAllowed) {
try {
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
window.console.error('AngularJS: disabling automatic bootstrap. <script> protocol indicates ' +
'an extension, document.location.href does not match.');
} catch (e) {
// Support: Safari 11 w/ Webdriver
// The console.error will throw and make the test fail
}
return;
}
config.strictDi = getNgAttribute(appElement, 'strict-di') !== null;
+3 -2
View File
@@ -7,7 +7,7 @@
htmlAnchorDirective,
inputDirective,
inputDirective,
hiddenInputBrowserCacheDirective,
formDirective,
scriptDirective,
selectDirective,
@@ -221,7 +221,8 @@ function publishExternalAPI(angular) {
ngModelOptions: ngModelOptionsDirective
}).
directive({
ngInclude: ngIncludeFillContentDirective
ngInclude: ngIncludeFillContentDirective,
input: hiddenInputBrowserCacheDirective
}).
directive(ngAttributeAliasDirectives).
directive(ngEventDirectives);
+3
View File
@@ -108,6 +108,9 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
if (url) {
var sameState = lastHistoryState === state;
// Normalize the inputted URL
url = urlResolve(url).href;
// Don't change anything if previous and current URLs and states match. This also prevents
// IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
// See https://github.com/angular/angular.js/commit/ffb2701
+11 -2
View File
@@ -2231,7 +2231,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
this.$$element.removeAttr(attrName);
} else {
if (SIMPLE_ATTR_NAME.test(attrName)) {
this.$$element.attr(attrName, value);
// jQuery skips special boolean attrs treatment in XML nodes for
// historical reasons and hence AngularJS cannot freely call
// `.attr(attrName, false) with such attributes. To avoid issues
// in XHTML, call `removeAttr` in such cases instead.
// See https://github.com/jquery/jquery/issues/4249
if (booleanKey && value === false) {
this.$$element.removeAttr(attrName);
} else {
this.$$element.attr(attrName, value);
}
} else {
setSpecialAttr(this.$$element[0], attrName, value);
}
@@ -3828,7 +3837,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
pre: function ngPropPreLinkFn(scope, $element) {
function applyPropValue() {
var propValue = ngPropGetter(scope);
$element.prop(propName, sanitizer(propValue));
$element[0][propName] = sanitizer(propValue);
}
applyPropValue();
+6 -3
View File
@@ -181,7 +181,6 @@
</file>
</example>
*
* @element INPUT
* @param {expression} ngDisabled If the {@link guide/expression expression} is truthy,
* then the `disabled` attribute will be set on the element
*/
@@ -408,7 +407,7 @@ forEach(ALIASED_ATTR, function(htmlAttr, ngAttr) {
// ng-src, ng-srcset, ng-href are interpolated
forEach(['src', 'srcset', 'href'], function(attrName) {
var normalized = directiveNormalize('ng-' + attrName);
ngAttributeAliasDirectives[normalized] = function() {
ngAttributeAliasDirectives[normalized] = ['$sce', function($sce) {
return {
priority: 99, // it needs to run after the attributes are interpolated
link: function(scope, element, attr) {
@@ -422,6 +421,10 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
propName = null;
}
// We need to sanitize the url at least once, in case it is a constant
// non-interpolated attribute.
attr.$set(normalized, $sce.getTrustedMediaUrl(attr[normalized]));
attr.$observe(normalized, function(value) {
if (!value) {
if (attrName === 'href') {
@@ -441,5 +444,5 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
});
}
};
};
}];
});
+115 -31
View File
@@ -909,8 +909,10 @@ var inputType = {
*
* <div class="alert alert-warning">
* **Note:** `input[email]` uses a regex to validate email addresses that is derived from the regex
* used in Chromium. If you need stricter validation (e.g. requiring a top-level domain), you can
* use `ng-pattern` or modify the built-in validators (see the {@link guide/forms Forms guide})
* used in Chromium, which may not fulfill your app's requirements.
* If you need stricter (e.g. requiring a top-level domain), or more relaxed validation
* (e.g. allowing IPv6 address literals) you can use `ng-pattern` or
* modify the built-in validators (see the {@link guide/forms Forms guide}).
* </div>
*
* @param {string} ngModel Assignable AngularJS expression to data-bind to.
@@ -1497,7 +1499,7 @@ function createDateParser(regexp, mapping) {
}
function createDateInputType(type, regexp, parseDate, format) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter) {
return function dynamicDateInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
badInputChecker(scope, element, attr, ctrl, type);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
@@ -1540,24 +1542,34 @@ function createDateInputType(type, regexp, parseDate, format) {
});
if (isDefined(attr.min) || attr.ngMin) {
var minVal;
var minVal = attr.min || $parse(attr.ngMin)(scope);
var parsedMinVal = parseObservedDateValue(minVal);
ctrl.$validators.min = function(value) {
return !isValidDate(value) || isUndefined(minVal) || parseDate(value) >= minVal;
return !isValidDate(value) || isUndefined(parsedMinVal) || parseDate(value) >= parsedMinVal;
};
attr.$observe('min', function(val) {
minVal = parseObservedDateValue(val);
ctrl.$validate();
if (val !== minVal) {
parsedMinVal = parseObservedDateValue(val);
minVal = val;
ctrl.$validate();
}
});
}
if (isDefined(attr.max) || attr.ngMax) {
var maxVal;
var maxVal = attr.max || $parse(attr.ngMax)(scope);
var parsedMaxVal = parseObservedDateValue(maxVal);
ctrl.$validators.max = function(value) {
return !isValidDate(value) || isUndefined(maxVal) || parseDate(value) <= maxVal;
return !isValidDate(value) || isUndefined(parsedMaxVal) || parseDate(value) <= parsedMaxVal;
};
attr.$observe('max', function(val) {
maxVal = parseObservedDateValue(val);
ctrl.$validate();
if (val !== maxVal) {
parsedMaxVal = parseObservedDateValue(val);
maxVal = val;
ctrl.$validate();
}
});
}
@@ -1709,50 +1721,68 @@ function isValidForStep(viewValue, stepBase, step) {
return (value - stepBase) % step === 0;
}
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser) {
function numberInputType(scope, element, attr, ctrl, $sniffer, $browser, $filter, $parse) {
badInputChecker(scope, element, attr, ctrl, 'number');
numberFormatterParser(ctrl);
baseInputType(scope, element, attr, ctrl, $sniffer, $browser);
var minVal;
var maxVal;
var parsedMinVal;
if (isDefined(attr.min) || attr.ngMin) {
var minVal = attr.min || $parse(attr.ngMin)(scope);
parsedMinVal = parseNumberAttrVal(minVal);
ctrl.$validators.min = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || isUndefined(minVal) || viewValue >= minVal;
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMinVal) || viewValue >= parsedMinVal;
};
attr.$observe('min', function(val) {
minVal = parseNumberAttrVal(val);
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
if (val !== minVal) {
parsedMinVal = parseNumberAttrVal(val);
minVal = val;
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
}
});
}
if (isDefined(attr.max) || attr.ngMax) {
var maxVal = attr.max || $parse(attr.ngMax)(scope);
var parsedMaxVal = parseNumberAttrVal(maxVal);
ctrl.$validators.max = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || isUndefined(maxVal) || viewValue <= maxVal;
return ctrl.$isEmpty(viewValue) || isUndefined(parsedMaxVal) || viewValue <= parsedMaxVal;
};
attr.$observe('max', function(val) {
maxVal = parseNumberAttrVal(val);
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
if (val !== maxVal) {
parsedMaxVal = parseNumberAttrVal(val);
maxVal = val;
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
}
});
}
if (isDefined(attr.step) || attr.ngStep) {
var stepVal;
var stepVal = attr.step || $parse(attr.ngStep)(scope);
var parsedStepVal = parseNumberAttrVal(stepVal);
ctrl.$validators.step = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || isUndefined(stepVal) ||
isValidForStep(viewValue, minVal || 0, stepVal);
return ctrl.$isEmpty(viewValue) || isUndefined(parsedStepVal) ||
isValidForStep(viewValue, parsedMinVal || 0, parsedStepVal);
};
attr.$observe('step', function(val) {
stepVal = parseNumberAttrVal(val);
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
if (val !== stepVal) {
parsedStepVal = parseNumberAttrVal(val);
stepVal = val;
ctrl.$validate();
}
});
}
}
@@ -1782,6 +1812,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
originalRender;
if (hasMinAttr) {
minVal = parseNumberAttrVal(attr.min);
ctrl.$validators.min = supportsRange ?
// Since all browsers set the input to a valid value, we don't need to check validity
function noopMinValidator() { return true; } :
@@ -1794,6 +1826,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
if (hasMaxAttr) {
maxVal = parseNumberAttrVal(attr.max);
ctrl.$validators.max = supportsRange ?
// Since all browsers set the input to a valid value, we don't need to check validity
function noopMaxValidator() { return true; } :
@@ -1806,6 +1840,8 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
if (hasStepAttr) {
stepVal = parseNumberAttrVal(attr.step);
ctrl.$validators.step = supportsRange ?
function nativeStepValidator() {
// Currently, only FF implements the spec on step change correctly (i.e. adjusting the
@@ -1827,7 +1863,13 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
// attribute value when the input is first rendered, so that the browser can adjust the
// input value based on the min/max value
element.attr(htmlAttrName, attr[htmlAttrName]);
attr.$observe(htmlAttrName, changeFn);
var oldVal = attr[htmlAttrName];
attr.$observe(htmlAttrName, function wrappedObserver(val) {
if (val !== oldVal) {
oldVal = val;
changeFn(val);
}
});
}
function minChange(val) {
@@ -1881,11 +1923,11 @@ function rangeInputType(scope, element, attr, ctrl, $sniffer, $browser) {
}
// Some browsers don't adjust the input value correctly, but set the stepMismatch error
if (supportsRange && ctrl.$viewValue !== element.val()) {
ctrl.$setViewValue(element.val());
} else {
if (!supportsRange) {
// TODO(matsko): implement validateLater to reduce number of validations
ctrl.$validate();
} else if (ctrl.$viewValue !== element.val()) {
ctrl.$setViewValue(element.val());
}
}
}
@@ -2193,6 +2235,48 @@ var inputDirective = ['$browser', '$sniffer', '$filter', '$parse',
}];
var hiddenInputBrowserCacheDirective = function() {
var valueProperty = {
configurable: true,
enumerable: false,
get: function() {
return this.getAttribute('value') || '';
},
set: function(val) {
this.setAttribute('value', val);
}
};
return {
restrict: 'E',
priority: 200,
compile: function(_, attr) {
if (lowercase(attr.type) !== 'hidden') {
return;
}
return {
pre: function(scope, element, attr, ctrls) {
var node = element[0];
// Support: Edge
// Moving the DOM around prevents autofillling
if (node.parentNode) {
node.parentNode.insertBefore(node, node.nextSibling);
}
// Support: FF, IE
// Avoiding direct assignment to .value prevents autofillling
if (Object.defineProperty) {
Object.defineProperty(node, 'value', valueProperty);
}
}
};
}
};
};
var CONSTANT_VALUE_REGEXP = /^(true|false|\d+)$/;
/**
+1
View File
@@ -562,6 +562,7 @@ NgModelController.prototype = {
* `$modelValue`, i.e. either the last parsed value or the last value set from the scope.
*/
$validate: function() {
// ignore $validate before model is initialized
if (isNumberNaN(this.$modelValue)) {
return;
+24 -20
View File
@@ -454,6 +454,13 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
return block.clone[block.clone.length - 1];
};
var trackByIdArrayFn = function($scope, key, value) {
return hashKey(value);
};
var trackByIdObjFn = function($scope, key) {
return key;
};
return {
restrict: 'A',
@@ -493,32 +500,23 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
aliasAs);
}
var trackByExpGetter, trackByIdExpFn, trackByIdArrayFn, trackByIdObjFn;
var hashFnLocals = {$id: hashKey};
var trackByIdExpFn;
if (trackByExp) {
trackByExpGetter = $parse(trackByExp);
} else {
trackByIdArrayFn = function(key, value) {
return hashKey(value);
};
trackByIdObjFn = function(key) {
return key;
var hashFnLocals = {$id: hashKey};
var trackByExpGetter = $parse(trackByExp);
trackByIdExpFn = function($scope, key, value, index) {
// assign key, value, and $index to the locals so that they can be used in hash functions
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
hashFnLocals[valueIdentifier] = value;
hashFnLocals.$index = index;
return trackByExpGetter($scope, hashFnLocals);
};
}
return function ngRepeatLink($scope, $element, $attr, ctrl, $transclude) {
if (trackByExpGetter) {
trackByIdExpFn = function(key, value, index) {
// assign key, value, and $index to the locals so that they can be used in hash functions
if (keyIdentifier) hashFnLocals[keyIdentifier] = key;
hashFnLocals[valueIdentifier] = value;
hashFnLocals.$index = index;
return trackByExpGetter($scope, hashFnLocals);
};
}
// Store a list of elements from previous run. This is a hash where key is the item from the
// iterator, and the value is objects with following properties.
// - scope: bound scope
@@ -572,7 +570,7 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
for (index = 0; index < collectionLength; index++) {
key = (collection === collectionKeys) ? index : collectionKeys[index];
value = collection[key];
trackById = trackByIdFn(key, value, index);
trackById = trackByIdFn($scope, key, value, index);
if (lastBlockMap[trackById]) {
// found previously seen block
block = lastBlockMap[trackById];
@@ -594,6 +592,12 @@ var ngRepeatDirective = ['$parse', '$animate', '$compile', function($parse, $ani
}
}
// Clear the value property from the hashFnLocals object to prevent a reference to the last value
// being leaked into the ngRepeatCompile function scope
if (hashFnLocals) {
hashFnLocals[valueIdentifier] = undefined;
}
// remove leftover items
for (var blockKey in lastBlockMap) {
block = lastBlockMap[blockKey];
+8 -1
View File
@@ -54,7 +54,14 @@
var ngStyleDirective = ngDirective(function(scope, element, attr) {
scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
forEach(oldStyles, function(val, style) { element.css(style, '');});
if (!newStyles) {
newStyles = {};
}
forEach(oldStyles, function(val, style) {
if (newStyles[style] == null) {
newStyles[style] = '';
}
});
}
if (newStyles) element.css(newStyles);
});
+97 -37
View File
@@ -62,24 +62,29 @@
* </file>
* </example>
*/
var requiredDirective = function() {
var requiredDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var value = attr.required || $parse(attr.ngRequired)(scope);
attr.required = true; // force truthy in case we are on non input element
ctrl.$validators.required = function(modelValue, viewValue) {
return !attr.required || !ctrl.$isEmpty(viewValue);
return !value || !ctrl.$isEmpty(viewValue);
};
attr.$observe('required', function() {
ctrl.$validate();
attr.$observe('required', function(newVal) {
if (value !== newVal) {
value = newVal;
ctrl.$validate();
}
});
}
};
};
}];
/**
* @ngdoc directive
@@ -162,36 +167,59 @@ var requiredDirective = function() {
* </file>
* </example>
*/
var patternDirective = function() {
var patternDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
compile: function(tElm, tAttr) {
var patternExp;
var parseFn;
var regexp, patternExp = attr.ngPattern || attr.pattern;
attr.$observe('pattern', function(regex) {
if (isString(regex) && regex.length > 0) {
regex = new RegExp('^' + regex + '$');
if (tAttr.ngPattern) {
patternExp = tAttr.ngPattern;
// ngPattern might be a scope expression, or an inlined regex, which is not parsable.
// We get value of the attribute here, so we can compare the old and the new value
// in the observer to avoid unnecessary validations
if (tAttr.ngPattern.charAt(0) === '/' && REGEX_STRING_REGEXP.test(tAttr.ngPattern)) {
parseFn = function() { return tAttr.ngPattern; };
} else {
parseFn = $parse(tAttr.ngPattern);
}
}
return function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var attrVal = attr.pattern;
if (attr.ngPattern) {
attrVal = parseFn(scope);
} else {
patternExp = attr.pattern;
}
if (regex && !regex.test) {
throw minErr('ngPattern')('noregexp',
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
regex, startingTag(elm));
}
var regexp = parsePatternAttr(attrVal, patternExp, elm);
regexp = regex || undefined;
ctrl.$validate();
});
attr.$observe('pattern', function(newVal) {
var oldRegexp = regexp;
ctrl.$validators.pattern = function(modelValue, viewValue) {
// HTML5 pattern constraint validates the input value, so we validate the viewValue
return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
regexp = parsePatternAttr(newVal, patternExp, elm);
if ((oldRegexp && oldRegexp.toString()) !== (regexp && regexp.toString())) {
ctrl.$validate();
}
});
ctrl.$validators.pattern = function(modelValue, viewValue) {
// HTML5 pattern constraint validates the input value, so we validate the viewValue
return ctrl.$isEmpty(viewValue) || isUndefined(regexp) || regexp.test(viewValue);
};
};
}
};
};
}];
/**
* @ngdoc directive
@@ -264,25 +292,29 @@ var patternDirective = function() {
* </file>
* </example>
*/
var maxlengthDirective = function() {
var maxlengthDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var maxlength = -1;
var maxlength = attr.maxlength || $parse(attr.ngMaxlength)(scope);
var maxlengthParsed = parseLength(maxlength);
attr.$observe('maxlength', function(value) {
var intVal = toInt(value);
maxlength = isNumberNaN(intVal) ? -1 : intVal;
ctrl.$validate();
if (maxlength !== value) {
maxlengthParsed = parseLength(value);
maxlength = value;
ctrl.$validate();
}
});
ctrl.$validators.maxlength = function(modelValue, viewValue) {
return (maxlength < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlength);
return (maxlengthParsed < 0) || ctrl.$isEmpty(viewValue) || (viewValue.length <= maxlengthParsed);
};
}
};
};
}];
/**
* @ngdoc directive
@@ -353,21 +385,49 @@ var maxlengthDirective = function() {
* </file>
* </example>
*/
var minlengthDirective = function() {
var minlengthDirective = ['$parse', function($parse) {
return {
restrict: 'A',
require: '?ngModel',
link: function(scope, elm, attr, ctrl) {
if (!ctrl) return;
var minlength = 0;
var minlength = attr.minlength || $parse(attr.ngMinlength)(scope);
var minlengthParsed = parseLength(minlength) || -1;
attr.$observe('minlength', function(value) {
minlength = toInt(value) || 0;
ctrl.$validate();
if (minlength !== value) {
minlengthParsed = parseLength(value) || -1;
minlength = value;
ctrl.$validate();
}
});
ctrl.$validators.minlength = function(modelValue, viewValue) {
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlength;
return ctrl.$isEmpty(viewValue) || viewValue.length >= minlengthParsed;
};
}
};
};
}];
function parsePatternAttr(regex, patternExp, elm) {
if (!regex) return undefined;
if (isString(regex)) {
regex = new RegExp('^' + regex + '$');
}
if (!regex.test) {
throw minErr('ngPattern')('noregexp',
'Expected {0} to be a RegExp but was {1}. Element: {2}', patternExp,
regex, startingTag(elm));
}
return regex;
}
function parseLength(val) {
var intVal = toInt(val);
return isNumberNaN(intVal) ? -1 : intVal;
}
+1 -1
View File
@@ -242,7 +242,7 @@ function $InterpolateProvider() {
// Provide a quick exit and simplified result function for text with no interpolation
if (!text.length || text.indexOf(startSymbol) === -1) {
if (mustHaveExpression && !contextAllowsConcatenation) return;
if (mustHaveExpression) return;
var unescapedText = unescapeText(text);
if (contextAllowsConcatenation) {
+7 -1
View File
@@ -683,5 +683,11 @@ function markQStateExceptionHandled(state) {
state.pur = true;
}
function markQExceptionHandled(q) {
markQStateExceptionHandled(q.$$state);
// Built-in `$q` promises will always have a `$$state` property. This check is to allow
// overwriting `$q` with a different promise library (e.g. Bluebird + angular-bluebird-promises).
// (Currently, this is the only method that might be called with a promise, even if it is not
// created by the built-in `$q`.)
if (q.$$state) {
markQStateExceptionHandled(q.$$state);
}
}
+13 -1
View File
@@ -10,6 +10,12 @@ var urlParsingNode = window.document.createElement('a');
var originUrl = urlResolve(window.location.href);
var baseUrlParsingNode;
urlParsingNode.href = 'http://[::1]';
// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview)
// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets
// when parsed out of an anchor element.
var ipv6InBrackets = urlParsingNode.hostname === '[::1]';
/**
*
@@ -72,13 +78,19 @@ function urlResolve(url) {
urlParsingNode.setAttribute('href', href);
var hostname = urlParsingNode.hostname;
if (!ipv6InBrackets && hostname.indexOf(':') > -1) {
hostname = '[' + hostname + ']';
}
return {
href: urlParsingNode.href,
protocol: urlParsingNode.protocol ? urlParsingNode.protocol.replace(/:$/, '') : '',
host: urlParsingNode.host,
search: urlParsingNode.search ? urlParsingNode.search.replace(/^\?/, '') : '',
hash: urlParsingNode.hash ? urlParsingNode.hash.replace(/^#/, '') : '',
hostname: urlParsingNode.hostname,
hostname: hostname,
port: urlParsingNode.port,
pathname: (urlParsingNode.pathname.charAt(0) === '/')
? urlParsingNode.pathname
+1 -1
View File
@@ -57,11 +57,11 @@
"applyInlineStyle": false,
"assertArg": false,
"blockKeyframeAnimations": false,
"blockTransitions": false,
"clearGeneratedClasses": false,
"concatWithSpace": false,
"extractElementNode": false,
"getDomNode": false,
"helpers": false,
"mergeAnimationDetails": false,
"mergeClasses": false,
"packageStyles": false,
+4 -4
View File
@@ -560,7 +560,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
// that if there is no transition defined then nothing will happen and this will also allow
// other transitions to be stacked on top of each other without any chopping them out.
if (isFirst && !options.skipBlocking) {
blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
helpers.blockTransitions(node, SAFE_FAST_FORWARD_DURATION_VALUE);
}
var timings = computeTimings(node, fullClassName, cacheKey, !isStructural);
@@ -646,7 +646,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
if (flags.blockTransition || flags.blockKeyframeAnimation) {
applyBlocking(maxDuration);
} else if (!options.skipBlocking) {
blockTransitions(node, false);
helpers.blockTransitions(node, false);
}
// TODO(matsko): for 1.5 change this code to have an animator object for better debugging
@@ -699,7 +699,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
}
blockKeyframeAnimations(node, false);
blockTransitions(node, false);
helpers.blockTransitions(node, false);
forEach(temporaryStyles, function(entry) {
// There is only one way to remove inline style properties entirely from elements.
@@ -750,7 +750,7 @@ var $AnimateCssProvider = ['$animateProvider', /** @this */ function($animatePro
function applyBlocking(duration) {
if (flags.blockTransition) {
blockTransitions(node, duration);
helpers.blockTransitions(node, duration);
}
if (flags.blockKeyframeAnimation) {
+2 -1
View File
@@ -92,7 +92,8 @@ var ngAnimateSwapDirective = ['$animate', function($animate) {
restrict: 'A',
transclude: 'element',
terminal: true,
priority: 600, // we use 600 here to ensure that the directive is caught before others
priority: 550, // We use 550 here to ensure that the directive is caught before others,
// but after `ngIf` (at priority 600).
link: function(scope, $element, attrs, ctrl, $transclude) {
var previousElement, previousScope;
scope.$watchCollection(attrs.ngAnimateSwap || attrs['for'], function(value) {
+11 -9
View File
@@ -329,15 +329,6 @@ function clearGeneratedClasses(element, options) {
}
}
function blockTransitions(node, duration) {
// we use a negative delay value since it performs blocking
// yet it doesn't kill any existing transitions running on the
// same element which makes this safe for class-based animations
var value = duration ? '-' + duration + 's' : '';
applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
return [TRANSITION_DELAY_PROP, value];
}
function blockKeyframeAnimations(node, applyBlock) {
var value = applyBlock ? 'paused' : '';
var key = ANIMATION_PROP + ANIMATION_PLAYSTATE_KEY;
@@ -356,3 +347,14 @@ function concatWithSpace(a,b) {
if (!b) return a;
return a + ' ' + b;
}
var helpers = {
blockTransitions: function(node, duration) {
// we use a negative delay value since it performs blocking
// yet it doesn't kill any existing transitions running on the
// same element which makes this safe for class-based animations
var value = duration ? '-' + duration + 's' : '';
applyInlineStyle(node, [TRANSITION_DELAY_PROP, value]);
return [TRANSITION_DELAY_PROP, value];
}
};
+1 -1
View File
@@ -390,7 +390,7 @@ ngAriaModule.directive('ngShow', ['$aria', function($aria) {
if (keyCode === 13 || keyCode === 32) {
// If the event is triggered on a non-interactive element ...
if (nodeBlackList.indexOf(event.target.nodeName) === -1) {
if (nodeBlackList.indexOf(event.target.nodeName) === -1 && !event.target.isContentEditable) {
// ... prevent the default browser behavior (e.g. scrolling when pressing spacebar)
// See https://github.com/angular/angular.js/issues/16664
event.preventDefault();
+4 -4
View File
@@ -215,10 +215,10 @@ var noop;
var toJson;
var $$stringify;
var module = window['angular']['module']('ngMessageFormat', ['ng']);
module['info']({ 'angularVersion': '"NG_VERSION_FULL"' });
module['factory']('$$messageFormat', $$MessageFormatFactory);
module['config'](['$provide', function($provide) {
var ngModule = window['angular']['module']('ngMessageFormat', ['ng']);
ngModule['info']({ 'angularVersion': '"NG_VERSION_FULL"' });
ngModule['factory']('$$messageFormat', $$MessageFormatFactory);
ngModule['config'](['$provide', function($provide) {
$interpolateMinErr = window['angular']['$interpolateMinErr'];
isFunction = window['angular']['isFunction'];
noop = window['angular']['noop'];
+9 -9
View File
@@ -1619,12 +1619,12 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
/**
* @ngdoc method
* @name $httpBackend#matchLatestDefinition
* @name $httpBackend#matchLatestDefinitionEnabled
* @description
* This method can be used to change which mocked responses `$httpBackend` returns, when defining
* them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
* By default, `$httpBackend` returns the first definition that matches. When setting
* `$http.matchLatestDefinition(true)`, it will use the last response that matches, i.e. the
* `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the
* one that was added last.
*
* ```js
@@ -1632,7 +1632,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* hb.when('GET', '/url1').respond(201, 'another', {});
* hb('GET', '/url1'); // receives "content"
*
* $http.matchLatestDefinition(true)
* $http.matchLatestDefinitionEnabled(true)
* hb('GET', '/url1'); // receives "another"
*
* hb.when('GET', '/url1').respond(201, 'onemore', {});
@@ -1641,7 +1641,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
*
* This is useful if a you have a default response that is overriden inside specific tests.
*
* Note that different from config methods on providers, `matchLatestDefinition()` can be changed
* Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed
* even when the application is already running.
*
* @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
@@ -1650,7 +1650,7 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
* as a getter
*/
$httpBackend.matchLatestDefinitionEnabled = function(value) {
if (isDefined(value)) {
if (angular.isDefined(value)) {
matchLatestDefinition = value;
return this;
} else {
@@ -2919,13 +2919,13 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
*/
/**
* @ngdoc method
* @name $httpBackend#matchLatestDefinition
* @name $httpBackend#matchLatestDefinitionEnabled
* @module ngMockE2E
* @description
* This method can be used to change which mocked responses `$httpBackend` returns, when defining
* them with {@link ngMock.$httpBackend#when $httpBackend.when()} (and shortcut methods).
* By default, `$httpBackend` returns the first definition that matches. When setting
* `$http.matchLatestDefinition(true)`, it will use the last response that matches, i.e. the
* `$http.matchLatestDefinitionEnabled(true)`, it will use the last response that matches, i.e. the
* one that was added last.
*
* ```js
@@ -2933,7 +2933,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
* hb.when('GET', '/url1').respond(201, 'another', {});
* hb('GET', '/url1'); // receives "content"
*
* $http.matchLatestDefinition(true)
* $http.matchLatestDefinitionEnabled(true)
* hb('GET', '/url1'); // receives "another"
*
* hb.when('GET', '/url1').respond(201, 'onemore', {});
@@ -2942,7 +2942,7 @@ angular.module('ngMockE2E', ['ng']).config(['$provide', function($provide) {
*
* This is useful if a you have a default response that is overriden inside specific tests.
*
* Note that different from config methods on providers, `matchLatestDefinition()` can be changed
* Note that different from config methods on providers, `matchLatestDefinitionEnabled()` can be changed
* even when the application is already running.
*
* @param {Boolean=} value value to set, either `true` or `false`. Default is `false`.
+6
View File
@@ -6,6 +6,7 @@
* @ngdoc module
* @name ngParseExt
* @packageName angular-parse-ext
*
* @description
*
* The `ngParseExt` module provides functionality to allow Unicode characters in
@@ -15,6 +16,11 @@
* to be used as an identifier in an AngularJS expression. ES6 delegates some of the identifier
* rules definition to Unicode, this module uses ES6 and Unicode 8.0 identifiers convention.
*
* <div class="alert alert-warning">
* You cannot use Unicode characters for variable names in the {@link ngRepeat} or {@link ngOptions}
* expressions (e.g. `ng-repeat="f in поля"`), because even with `ngParseExt` included, these
* special expressions are not parsed by the {@link $parse} service.
* </div>
*/
/* global angularParseExtModule: true,
+13
View File
@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html ng-app="test">
<body ng-class="{hacked: internalFnCalled}">
<form>
<input id="input1" type="hidden" value="{{value}}" />
<input id="input2" type="hidden" ng-value="value" />
<textarea ng-model="value"></textarea>
</form>
<script src="angular.js"></script>
<script src="script.js"></script>
</body>
</html>
+11
View File
@@ -0,0 +1,11 @@
'use strict';
angular
.module('test', [])
.run(function($rootScope) {
$rootScope.internalFnCalled = false;
$rootScope.internalFn = function() {
$rootScope.internalFnCalled = true;
};
});
+1 -1
View File
@@ -7,7 +7,7 @@ describe('SCE URL policy when base tags are present', function() {
it('allows the page URL (location.href)', function() {
expectToBeTrusted(browser.getLocationAbsUrl(), true);
expectToBeTrusted(browser.getCurrentUrl(), true);
});
it('blocks off-origin URLs', function() {
+68
View File
@@ -14,4 +14,72 @@ describe('hidden thingy', function() {
var expectedValue = browser.params.browser === 'safari' ? '{{ 7 * 6 }}' : '';
expect(element(by.css('input')).getAttribute('value')).toEqual(expectedValue);
});
it('should prevent browser autofill on browser.refresh', function() {
loadFixture('back2dom');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('body')).getAttribute('class')).toBe('');
browser.refresh();
expect(element(by.css('body')).getAttribute('class')).toBe('');
});
it('should prevent browser autofill on location.reload', function() {
loadFixture('back2dom');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('body')).getAttribute('class')).toBe('');
browser.driver.executeScript('location.reload()');
expect(element(by.css('body')).getAttribute('class')).toBe('');
});
it('should prevent browser autofill on history.back', function() {
loadFixture('back2dom');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('body')).getAttribute('class')).toBe('');
loadFixture('sample');
browser.driver.executeScript('history.back()');
expect(element(by.css('body')).getAttribute('class')).toBe('');
});
it('should prevent browser autofill on history.forward', function() {
loadFixture('sample');
loadFixture('back2dom');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('');
element(by.css('textarea')).sendKeys('{{ internalFn() }}');
expect(element(by.css('#input1')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('#input2')).getAttribute('value')).toEqual('{{ internalFn() }}');
expect(element(by.css('body')).getAttribute('class')).toBe('');
browser.driver.executeScript('history.back()');
browser.driver.executeScript('history.forward()');
expect(element(by.css('body')).getAttribute('class')).toBe('');
});
});
+22
View File
@@ -67,6 +67,7 @@ afterEach(function() {
} else {
dump('LEAK', key, angular.toJson(value));
}
delete expando.data[key];
});
});
if (count) {
@@ -312,7 +313,28 @@ window.dump = function() {
function generateInputCompilerHelper(helper) {
beforeEach(function() {
helper.validationCounter = {};
module(function($compileProvider) {
$compileProvider.directive('validationSpy', function() {
return {
priority: 1,
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
var validationName = attrs.validationSpy;
var originalValidator = ctrl.$validators[validationName];
helper.validationCounter[validationName] = 0;
ctrl.$validators[validationName] = function(modelValue, viewValue) {
helper.validationCounter[validationName]++;
return originalValidator(modelValue, viewValue);
};
}
};
});
$compileProvider.directive('attrCapture', function() {
return function(scope, element, $attrs) {
helper.attrs = $attrs;
+93 -25
View File
@@ -11,8 +11,9 @@ function MockWindow(options) {
}
var events = {};
var timeouts = this.timeouts = [];
var locationHref = 'http://server/';
var committedHref = 'http://server/';
var locationHref = window.document.createElement('a');
var committedHref = window.document.createElement('a');
locationHref.href = committedHref.href = 'http://server/';
var mockWindow = this;
var msie = options.msie;
var ieState;
@@ -60,28 +61,28 @@ function MockWindow(options) {
this.location = {
get href() {
return committedHref;
return committedHref.href;
},
set href(value) {
locationHref = value;
locationHref.href = value;
mockWindow.history.state = null;
historyEntriesLength++;
if (!options.updateAsync) this.flushHref();
},
get hash() {
return getHash(committedHref);
return getHash(committedHref.href);
},
set hash(value) {
locationHref = replaceHash(locationHref, value);
locationHref.href = replaceHash(locationHref.href, value);
if (!options.updateAsync) this.flushHref();
},
replace: function(url) {
locationHref = url;
locationHref.href = url;
mockWindow.history.state = null;
if (!options.updateAsync) this.flushHref();
},
flushHref: function() {
committedHref = locationHref;
committedHref.href = locationHref.href;
}
};
@@ -91,13 +92,13 @@ function MockWindow(options) {
historyEntriesLength++;
},
replaceState: function(state, title, url) {
locationHref = url;
if (!options.updateAsync) committedHref = locationHref;
locationHref.href = url;
if (!options.updateAsync) committedHref.href = locationHref.href;
mockWindow.history.state = copy(state);
if (!options.updateAsync) this.flushHref();
},
flushHref: function() {
committedHref = locationHref;
committedHref.href = locationHref.href;
}
};
// IE 10-11 deserialize history.state on each read making subsequent reads
@@ -398,18 +399,18 @@ describe('browser', function() {
it('should return current location.href', function() {
fakeWindow.location.href = 'http://test.com';
expect(browser.url()).toEqual('http://test.com');
expect(browser.url()).toEqual('http://test.com/');
fakeWindow.location.href = 'https://another.com';
expect(browser.url()).toEqual('https://another.com');
expect(browser.url()).toEqual('https://another.com/');
});
it('should strip an empty hash fragment', function() {
fakeWindow.location.href = 'http://test.com#';
expect(browser.url()).toEqual('http://test.com');
fakeWindow.location.href = 'http://test.com/#';
expect(browser.url()).toEqual('http://test.com/');
fakeWindow.location.href = 'https://another.com#foo';
expect(browser.url()).toEqual('https://another.com#foo');
fakeWindow.location.href = 'https://another.com/#foo';
expect(browser.url()).toEqual('https://another.com/#foo');
});
it('should use history.pushState when available', function() {
@@ -417,7 +418,7 @@ describe('browser', function() {
browser.url('http://new.org');
expect(pushState).toHaveBeenCalledOnce();
expect(pushState.calls.argsFor(0)[2]).toEqual('http://new.org');
expect(pushState.calls.argsFor(0)[2]).toEqual('http://new.org/');
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
@@ -429,7 +430,7 @@ describe('browser', function() {
browser.url('http://new.org', true);
expect(replaceState).toHaveBeenCalledOnce();
expect(replaceState.calls.argsFor(0)[2]).toEqual('http://new.org');
expect(replaceState.calls.argsFor(0)[2]).toEqual('http://new.org/');
expect(pushState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
@@ -440,7 +441,7 @@ describe('browser', function() {
sniffer.history = false;
browser.url('http://new.org');
expect(fakeWindow.location.href).toEqual('http://new.org');
expect(fakeWindow.location.href).toEqual('http://new.org/');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
@@ -473,7 +474,7 @@ describe('browser', function() {
sniffer.history = false;
browser.url('http://new.org', true);
expect(locationReplace).toHaveBeenCalledWith('http://new.org');
expect(locationReplace).toHaveBeenCalledWith('http://new.org/');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
@@ -507,9 +508,9 @@ describe('browser', function() {
it('should not set URL when the URL is already set', function() {
var current = fakeWindow.location.href;
sniffer.history = false;
fakeWindow.location.href = 'dontchange';
fakeWindow.location.href = 'http://dontchange/';
browser.url(current);
expect(fakeWindow.location.href).toBe('dontchange');
expect(fakeWindow.location.href).toBe('http://dontchange/');
});
it('should not read out location.href if a reload was triggered but still allow to change the url', function() {
@@ -688,6 +689,73 @@ describe('browser', function() {
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should not do pushState with a URL using relative protocol', function() {
browser.url('http://server/');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
browser.url('//server');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should not do pushState with a URL only adding a trailing slash after domain', function() {
// A domain without a trailing /
browser.url('http://server');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
// A domain from something such as window.location.href with a trailing slash
browser.url('http://server/');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should not do pushState with a URL only removing a trailing slash after domain', function() {
// A domain from something such as window.location.href with a trailing slash
browser.url('http://server/');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
// A domain without a trailing /
browser.url('http://server');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should do pushState with a URL only adding a trailing slash after the path', function() {
browser.url('http://server/foo');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
browser.url('http://server/foo/');
expect(pushState).toHaveBeenCalledOnce();
expect(fakeWindow.location.href).toEqual('http://server/foo/');
});
it('should do pushState with a URL only removing a trailing slash after the path', function() {
browser.url('http://server/foo/');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
browser.url('http://server/foo');
expect(pushState).toHaveBeenCalledOnce();
expect(fakeWindow.location.href).toEqual('http://server/foo');
});
};
}
});
@@ -812,7 +880,7 @@ describe('browser', function() {
it('should not fire urlChange if changed by browser.url method', function() {
sniffer.history = false;
browser.onUrlChange(callback);
browser.url('http://new.com');
browser.url('http://new.com/');
fakeWindow.fire('hashchange');
expect(callback).not.toHaveBeenCalled();
@@ -1092,7 +1160,7 @@ describe('browser', function() {
it('should not interfere with legacy browser url replace behavior', function() {
inject(function($rootScope) {
var current = fakeWindow.location.href;
var newUrl = 'notyet';
var newUrl = 'http://notyet/';
sniffer.history = false;
expect(historyEntriesLength).toBe(1);
browser.url(newUrl, true);
+93 -7
View File
@@ -3577,6 +3577,15 @@ describe('$compile', function() {
})
);
it('should support non-interpolated `src` and `data-src` on the same element',
inject(function($rootScope, $compile) {
var element = $compile('<img src="abc" data-src="123">')($rootScope);
expect(element.attr('src')).toEqual('abc');
expect(element.attr('data-src')).toEqual('123');
$rootScope.$digest();
expect(element.attr('src')).toEqual('abc');
expect(element.attr('data-src')).toEqual('123');
}));
it('should call observer only when the attribute value changes', function() {
module(function() {
@@ -4213,6 +4222,40 @@ describe('$compile', function() {
expect(element.attr('ng-my-attr')).toBeUndefined();
});
it('should set the value to lowercased keys for boolean attrs', function() {
attr.$set('disabled', 'value');
expect(element.attr('disabled')).toEqual('disabled');
element.removeAttr('disabled');
attr.$set('dISaBlEd', 'VaLuE');
expect(element.attr('disabled')).toEqual('disabled');
});
it('should call removeAttr for boolean attrs when value is `false`', function() {
attr.$set('disabled', 'value');
spyOn(jqLite.prototype, 'attr').and.callThrough();
spyOn(jqLite.prototype, 'removeAttr').and.callThrough();
attr.$set('disabled', false);
expect(element.attr).not.toHaveBeenCalled();
expect(element.removeAttr).toHaveBeenCalledWith('disabled');
expect(element.attr('disabled')).toEqual(undefined);
attr.$set('disabled', 'value');
element.attr.calls.reset();
element.removeAttr.calls.reset();
attr.$set('dISaBlEd', false);
expect(element.attr).not.toHaveBeenCalled();
expect(element.removeAttr).toHaveBeenCalledWith('disabled');
expect(element.attr('disabled')).toEqual(undefined);
});
it('should not set DOM element attr if writeAttr false', function() {
attr.$set('test', 'value', false);
@@ -11703,37 +11746,37 @@ describe('$compile', function() {
// All interpolations are disallowed.
$rootScope.onClickJs = '';
expect(function() {
$compile('<button onclick="{{onClickJs}}"></script>');
$compile('<button onclick="{{onClickJs}}"></button>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ONCLICK="{{onClickJs}}"></script>');
$compile('<button ONCLICK="{{onClickJs}}"></button>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ng-attr-onclick="{{onClickJs}}"></script>');
$compile('<button ng-attr-onclick="{{onClickJs}}"></button>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
expect(function() {
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></script>');
$compile('<button ng-attr-ONCLICK="{{onClickJs}}"></button>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Interpolations for HTML DOM event attributes are disallowed');
}));
it('should pass through arbitrary values on onXYZ event attributes that contain a hyphen', inject(function($compile, $rootScope) {
element = $compile('<button on-click="{{onClickJs}}"></script>')($rootScope);
element = $compile('<button on-click="{{onClickJs}}"></button>')($rootScope);
$rootScope.onClickJs = 'javascript:doSomething()';
$rootScope.$apply();
expect(element.attr('on-click')).toEqual('javascript:doSomething()');
}));
it('should pass through arbitrary values on "on" and "data-on" attributes', inject(function($compile, $rootScope) {
element = $compile('<button data-on="{{dataOnVar}}"></script>')($rootScope);
element = $compile('<button data-on="{{dataOnVar}}"></button>')($rootScope);
$rootScope.dataOnVar = 'data-on text';
$rootScope.$apply();
expect(element.attr('data-on')).toEqual('data-on text');
element = $compile('<button on="{{onVar}}"></script>')($rootScope);
element = $compile('<button on="{{onVar}}"></button>')($rootScope);
$rootScope.onVar = 'on text';
$rootScope.$apply();
expect(element.attr('on')).toEqual('on text');
@@ -12084,6 +12127,49 @@ describe('$compile', function() {
expect(element.attr('test6')).toBe('Misko');
}));
describe('with media url attributes', function() {
it('should work with interpolated ng-attr-src', inject(function() {
$rootScope.name = 'some-image.png';
element = $compile('<img ng-attr-src="{{name}}">')($rootScope);
expect(element.attr('src')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('src')).toBe('some-image.png');
$rootScope.name = 'other-image.png';
$rootScope.$digest();
expect(element.attr('src')).toBe('other-image.png');
}));
it('should work with interpolated ng-attr-data-src', inject(function() {
$rootScope.name = 'some-image.png';
element = $compile('<img ng-attr-data-src="{{name}}">')($rootScope);
expect(element.attr('data-src')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('data-src')).toBe('some-image.png');
$rootScope.name = 'other-image.png';
$rootScope.$digest();
expect(element.attr('data-src')).toBe('other-image.png');
}));
it('should work alongside constant [src]-attribute and [ng-attr-data-src] attributes', inject(function() {
$rootScope.name = 'some-image.png';
element = $compile('<img src="constant.png" ng-attr-data-src="{{name}}">')($rootScope);
expect(element.attr('data-src')).toBeUndefined();
$rootScope.$digest();
expect(element.attr('src')).toBe('constant.png');
expect(element.attr('data-src')).toBe('some-image.png');
$rootScope.name = 'other-image.png';
$rootScope.$digest();
expect(element.attr('src')).toBe('constant.png');
expect(element.attr('data-src')).toBe('other-image.png');
}));
});
describe('when an attribute has a dash-separated name', function() {
it('should work with different prefixes', inject(function() {
$rootScope.name = 'JamieMason';
+303 -2
View File
@@ -839,6 +839,22 @@ describe('input', function() {
expect($rootScope.form.alias.$error.min).toBeFalsy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="month" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="month" ng-model="value" validation-spy="min" ng-min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
});
describe('max', function() {
@@ -898,6 +914,22 @@ describe('input', function() {
expect($rootScope.form.alias.$error.max).toBeFalsy();
expect($rootScope.form.alias.$valid).toBeTruthy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="month" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="month" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
});
});
@@ -1114,6 +1146,22 @@ describe('input', function() {
expect($rootScope.form.alias.$error.min).toBeFalsy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="week" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="week" ng-model="value" validation-spy="min" ng-min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
});
describe('max', function() {
@@ -1176,6 +1224,22 @@ describe('input', function() {
expect($rootScope.form.alias.$error.max).toBeFalsy();
expect($rootScope.form.alias.$valid).toBeTruthy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="week" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="week" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
});
});
@@ -1506,6 +1570,23 @@ describe('input', function() {
expect($rootScope.form.alias.$error.min).toBeFalsy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="datetime-local" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="datetime-local" ng-model="value" validation-spy="min" ng-min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
});
describe('max', function() {
@@ -1565,6 +1646,22 @@ describe('input', function() {
expect($rootScope.form.alias.$error.max).toBeFalsy();
expect($rootScope.form.alias.$valid).toBeTruthy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="datetime-local" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="datetime-local" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
});
@@ -1972,6 +2069,22 @@ describe('input', function() {
expect($rootScope.form.alias.$error.min).toBeFalsy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="time" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="time" ng-model="value" validation-spy="min" ng-min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
});
describe('max', function() {
@@ -2019,6 +2132,22 @@ describe('input', function() {
expect($rootScope.form.alias.$error.max).toBeFalsy();
expect($rootScope.form.alias.$valid).toBeTruthy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="time" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="time" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
});
@@ -2361,6 +2490,26 @@ describe('input', function() {
expect($rootScope.form.alias.$error.min).toBeFalsy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.minVal = '2000-01-01';
$rootScope.value = new Date(2010, 1, 1, 0, 0, 0);
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="date" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="date" ng-model="value" validation-spy="min" ng-min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
});
describe('max', function() {
@@ -2428,6 +2577,25 @@ describe('input', function() {
expect($rootScope.form.alias.$error.max).toBeFalsy();
expect($rootScope.form.alias.$valid).toBeTruthy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.maxVal = '2000-01-01';
$rootScope.value = new Date(2020, 1, 1, 0, 0, 0);
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="date" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="date" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
});
@@ -3063,6 +3231,18 @@ describe('input', function() {
$rootScope.$digest();
expect(inputElm).toBeValid();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.value = 5;
$rootScope.minVal = 3;
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="number" ng-model="value" validation-spy="min" min="{{ minVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
});
describe('ngMin', function() {
@@ -3131,6 +3311,17 @@ describe('input', function() {
$rootScope.$digest();
expect(inputElm).toBeValid();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.value = 5;
$rootScope.minVal = 3;
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="number" ng-model="value" validation-spy="min" ng-min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
});
@@ -3200,6 +3391,18 @@ describe('input', function() {
$rootScope.$digest();
expect(inputElm).toBeValid();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.value = 5;
$rootScope.maxVal = 3;
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="number" ng-model="value" validation-spy="max" name="alias" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
});
describe('ngMax', function() {
@@ -3268,6 +3471,17 @@ describe('input', function() {
$rootScope.$digest();
expect(inputElm).toBeValid();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.value = 5;
$rootScope.maxVal = 3;
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="number" ng-model="value" validation-spy="max" ng-max="maxVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
});
@@ -3364,7 +3578,7 @@ describe('input', function() {
expect(inputElm.val()).toBe('10');
expect(inputElm).toBeInvalid();
expect(ngModel.$error.step).toBe(true);
expect($rootScope.value).toBeUndefined();
expect($rootScope.value).toBe(10); // an initially invalid value should not be changed
helper.changeInputValueTo('15');
expect(inputElm).toBeValid();
@@ -3444,6 +3658,17 @@ describe('input', function() {
expect($rootScope.value).toBe(1.16);
}
);
it('should validate only once after compilation inside ngRepeat', function() {
$rootScope.step = 10;
$rootScope.value = 20;
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="number" ng-model="value" name="alias" ' + attrHtml + ' validation-spy="step" />' +
'</div>');
expect(helper.validationCounter.step).toBe(1);
});
});
});
@@ -3485,6 +3710,16 @@ describe('input', function() {
expect(inputElm).toBeValid();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.value = 'text';
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input ng-model="value" validation-spy="required" required />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.required).toBe(1);
});
});
describe('ngRequired', function() {
@@ -3534,6 +3769,17 @@ describe('input', function() {
expect($rootScope.value).toBeUndefined();
expect($rootScope.form.numberInput.$error.required).toBeFalsy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.value = 'text';
$rootScope.isRequired = true;
var inputElm = helper.compileInput('<div ng-repeat="input in [0]">' +
'<input ng-model="value" validation-spy="required" ng-required="isRequired" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.required).toBe(1);
});
});
describe('when the ngRequired expression initially evaluates to false', function() {
@@ -3848,6 +4094,17 @@ describe('input', function() {
expect(inputElm.val()).toBe('20');
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.minVal = 5;
$rootScope.value = 10;
helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="range" ng-model="value" validation-spy="min" min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
} else {
// input[type=range] will become type=text in browsers that don't support it
@@ -3926,6 +4183,16 @@ describe('input', function() {
expect(inputElm.val()).toBe('15');
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.minVal = 5;
$rootScope.value = 10;
helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="range" ng-model="value" validation-spy="min" min="minVal" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.min).toBe(1);
});
}
});
@@ -4006,6 +4273,17 @@ describe('input', function() {
expect(inputElm.val()).toBe('0');
});
it('should only validate once after compilation when inside ngRepeat and the value is valid', function() {
$rootScope.maxVal = 5;
$rootScope.value = 5;
helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="range" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
} else {
it('should validate if "range" is not implemented', function() {
var inputElm = helper.compileInput('<input type="range" ng-model="value" name="alias" max="10" />');
@@ -4081,6 +4359,17 @@ describe('input', function() {
expect(scope.value).toBe(5);
expect(inputElm.val()).toBe('5');
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.maxVal = 5;
$rootScope.value = 10;
helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="range" ng-model="value" validation-spy="max" max="{{ maxVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.max).toBe(1);
});
}
});
@@ -4183,6 +4472,18 @@ describe('input', function() {
expect(scope.value).toBe(10);
expect(scope.form.alias.$error.step).toBeFalsy();
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.stepVal = 5;
$rootScope.value = 10;
helper.compileInput('<div ng-repeat="input in [0]">' +
'<input type="range" ng-model="value" validation-spy="step" step="{{ stepVal }}" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.step).toBe(1);
});
} else {
it('should validate if "range" is not implemented', function() {
@@ -4269,7 +4570,7 @@ describe('input', function() {
expect(inputElm.val()).toBe('10');
expect(inputElm).toBeInvalid();
expect(ngModel.$error.step).toBe(true);
expect($rootScope.value).toBeUndefined();
expect($rootScope.value).toBe(10);
helper.changeInputValueTo('15');
expect(inputElm).toBeValid();
+2 -2
View File
@@ -68,8 +68,8 @@ describe('ngHref', function() {
}));
// Support: IE 9-11 only, Edge 12-15+
if (msie || /\bEdge\/[\d.]+\b/.test(window.navigator.userAgent)) {
// Support: IE 9-11 only, Edge 12-17
if (msie || /\bEdge\/1[2-7]\.[\d.]+\b/.test(window.navigator.userAgent)) {
// IE/Edge fail when setting a href to a URL containing a % that isn't a valid escape sequence
// See https://github.com/angular/angular.js/issues/13388
it('should throw error if ng-href contains a non-escaped percent symbol', inject(function($rootScope, $compile) {
-1
View File
@@ -863,7 +863,6 @@ describe('ngModel', function() {
});
});
describe('view -> model update', function() {
it('should always perform validations using the parsed model value', function() {
+37
View File
@@ -400,6 +400,43 @@ describe('ngRepeat', function() {
expect(element.find('input')[1].checked).toBe(true);
expect(element.find('input')[2].checked).toBe(true);
}));
it('should invoke track by with correct locals', function() {
scope.trackBy = jasmine.createSpy().and.callFake(function(k, v) {
return [k, v].join('');
});
element = $compile(
'<ul>' +
'<li ng-repeat="(k, v) in [1, 2] track by trackBy(k, v)"></li>' +
'</ul>')(scope);
scope.$digest();
expect(scope.trackBy).toHaveBeenCalledTimes(2);
expect(scope.trackBy.calls.argsFor(0)).toEqual([0, 1]);
expect(scope.trackBy.calls.argsFor(1)).toEqual([1, 2]);
});
// https://github.com/angular/angular.js/issues/16776
it('should invoke nested track by with correct locals', function() {
scope.trackBy = jasmine.createSpy().and.callFake(function(k1, v1, k2, v2) {
return [k1, v1, k2, v2].join('');
});
element = $compile(
'<ul>' +
'<li ng-repeat="(k1, v1) in [1, 2]">' +
'<div ng-repeat="(k2, v2) in [3, 4] track by trackBy(k1, v1, k2, v2)"></div>' +
'</li>' +
'</ul>')(scope);
scope.$digest();
expect(scope.trackBy).toHaveBeenCalledTimes(4);
expect(scope.trackBy.calls.argsFor(0)).toEqual([0, 1, 0, 3]);
expect(scope.trackBy.calls.argsFor(1)).toEqual([0, 1, 1, 4]);
expect(scope.trackBy.calls.argsFor(2)).toEqual([1, 2, 0, 3]);
expect(scope.trackBy.calls.argsFor(3)).toEqual([1, 2, 1, 4]);
});
});
describe('alias as', function() {
+14
View File
@@ -78,6 +78,20 @@ describe('ngSrc', function() {
expect(element.prop('src')).toEqual('http://somewhere/abc');
}));
}
it('should work with `src` attribute on the same element', inject(function($rootScope, $compile) {
$rootScope.imageUrl = 'dynamic';
element = $compile('<img ng-src="{{imageUrl}}" src="static">')($rootScope);
expect(element.attr('src')).toBe('static');
$rootScope.$digest();
expect(element.attr('src')).toBe('dynamic');
dealoc(element);
element = $compile('<img src="static" ng-src="{{imageUrl}}">')($rootScope);
expect(element.attr('src')).toBe('static');
$rootScope.$digest();
expect(element.attr('src')).toBe('dynamic');
}));
});
describe('iframe[ng-src]', function() {
+33
View File
@@ -120,5 +120,38 @@ describe('ngStyle', function() {
expect(element.css(preCompStyle)).not.toBe('88px');
expect(element.css(postCompStyle)).not.toBe('99px');
});
it('should clear style when the new model is null', function() {
scope.styleObj = {'height': '99px', 'width': '88px'};
scope.$apply();
expect(element.css(preCompStyle)).toBe('88px');
expect(element.css(postCompStyle)).toBe('99px');
scope.styleObj = null;
scope.$apply();
expect(element.css(preCompStyle)).not.toBe('88px');
expect(element.css(postCompStyle)).not.toBe('99px');
});
it('should clear style when the value is undefined or null', function() {
scope.styleObj = {'height': '99px', 'width': '88px'};
scope.$apply();
expect(element.css(preCompStyle)).toBe('88px');
expect(element.css(postCompStyle)).toBe('99px');
scope.styleObj = {'height': undefined, 'width': null};
scope.$apply();
expect(element.css(preCompStyle)).not.toBe('88px');
expect(element.css(postCompStyle)).not.toBe('99px');
});
it('should set style when the value is zero', function() {
scope.styleObj = {'height': '99px', 'width': '88px'};
scope.$apply();
expect(element.css(preCompStyle)).toBe('88px');
expect(element.css(postCompStyle)).toBe('99px');
scope.styleObj = {'height': 0, 'width': 0};
scope.$apply();
expect(element.css(preCompStyle)).toBe('0px');
expect(element.css(postCompStyle)).toBe('0px');
});
});
});
+119 -1
View File
@@ -230,6 +230,29 @@ describe('validators', function() {
expect(ctrl.$error.pattern).toBe(true);
expect(ctrlNg.$error.pattern).toBe(true);
}));
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.pattern = /\d{4}/;
helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" pattern="\\d{4}" validation-spy="pattern" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.pattern).toBe(1);
helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" ng-pattern="pattern" validation-spy="pattern" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.pattern).toBe(1);
});
});
@@ -312,9 +335,31 @@ describe('validators', function() {
expect(ctrl.$error.minlength).toBe(true);
expect(ctrlNg.$error.minlength).toBe(true);
}));
});
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.minlength = 5;
var element = helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" minlength="{{minlength}}" validation-spy="minlength" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.minlength).toBe(1);
element = helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" ng-minlength="minlength" validation-spy="minlength" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.minlength).toBe(1);
});
});
describe('maxlength', function() {
it('should invalidate values that are longer than the given maxlength', function() {
@@ -500,6 +545,29 @@ describe('validators', function() {
expect(ctrl.$error.maxlength).toBe(true);
expect(ctrlNg.$error.maxlength).toBe(true);
}));
it('should only validate once after compilation when inside ngRepeat', function() {
$rootScope.maxlength = 5;
var element = helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" maxlength="{{maxlength}}" validation-spy="maxlength" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.maxlength).toBe(1);
element = helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" ng-maxlength="maxlength" validation-spy="maxlength" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.maxlength).toBe(1);
});
});
@@ -626,5 +694,55 @@ describe('validators', function() {
expect(ctrl.$error.required).toBe(true);
expect(ctrlNg.$error.required).toBe(true);
}));
it('should validate only once after compilation when inside ngRepeat', function() {
helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" required validation-spy="required" />' +
'</div>');
$rootScope.$digest();
expect(helper.validationCounter.required).toBe(1);
});
it('should validate only once after compilation when inside ngRepeat and ngRequired is true', function() {
$rootScope.isRequired = true;
helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" ng-required="isRequired" validation-spy="required" />' +
'</div>');
expect(helper.validationCounter.required).toBe(1);
});
it('should validate only once after compilation when inside ngRepeat and ngRequired is false', function() {
$rootScope.isRequired = false;
helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-model="value" ng-required="isRequired" validation-spy="required" />' +
'</div>');
expect(helper.validationCounter.required).toBe(1);
});
it('should validate once when inside ngRepeat, and set the "required" error when ngRequired is false by default', function() {
$rootScope.isRequired = false;
$rootScope.refs = {};
var elm = helper.compileInput(
'<div ng-repeat="input in [0]">' +
'<input type="text" ng-ref="refs.input" ng-ref-read="ngModel" ng-model="value" ng-required="isRequired" validation-spy="required" />' +
'</div>');
expect(helper.validationCounter.required).toBe(1);
expect($rootScope.refs.input.$error.required).toBeUndefined();
});
});
});
+30
View File
@@ -155,4 +155,34 @@ describe('ngOn* event binding', function() {
expect(attrs.$attr.ngOnTitle).toBe('ng-on-title');
});
});
it('should correctly bind to kebab-cased event names', inject(function($compile, $rootScope) {
var element = $compile('<span ng-on-foo-bar="cb()"></span>')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('foobar');
element.triggerHandler('fooBar');
element.triggerHandler('foo_bar');
element.triggerHandler('foo:bar');
expect(cb).not.toHaveBeenCalled();
element.triggerHandler('foo-bar');
expect(cb).toHaveBeenCalled();
}));
it('should correctly bind to camelCased event names', inject(function($compile, $rootScope) {
var element = $compile('<span ng-on-foo_bar="cb()"></span>')($rootScope);
var cb = $rootScope.cb = jasmine.createSpy('ng-on cb');
$rootScope.$digest();
element.triggerHandler('foobar');
element.triggerHandler('foo-bar');
element.triggerHandler('foo_bar');
element.triggerHandler('foo:bar');
expect(cb).not.toHaveBeenCalled();
element.triggerHandler('fooBar');
expect(cb).toHaveBeenCalled();
}));
});
+44 -2
View File
@@ -49,6 +49,48 @@ describe('ngProp*', function() {
expect(element.prop('asdf')).toBe(true);
}));
// https://github.com/angular/angular.js/issues/16797
it('should support falsy property values', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-text="myText" />')($rootScope);
// Initialize to truthy value
$rootScope.myText = 'abc';
$rootScope.$digest();
expect(element.prop('text')).toBe('abc');
// Assert various falsey values get assigned to the property
$rootScope.myText = '';
$rootScope.$digest();
expect(element.prop('text')).toBe('');
$rootScope.myText = 0;
$rootScope.$digest();
expect(element.prop('text')).toBe(0);
$rootScope.myText = false;
$rootScope.$digest();
expect(element.prop('text')).toBe(false);
$rootScope.myText = undefined;
$rootScope.$digest();
expect(element.prop('text')).toBeUndefined();
$rootScope.myText = null;
$rootScope.$digest();
expect(element.prop('text')).toBe(null);
}));
it('should directly map special properties (class)', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-class="myText" />')($rootScope);
$rootScope.myText = 'abc';
$rootScope.$digest();
expect(element[0].class).toBe('abc');
expect(element).not.toHaveClass('abc');
}));
it('should not use jQuery .prop() to avoid jQuery propFix/hooks', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-class="myText" />')($rootScope);
spyOn(jqLite.prototype, 'prop');
$rootScope.myText = 'abc';
$rootScope.$digest();
expect(jqLite.prototype.prop).not.toHaveBeenCalled();
}));
it('should support mixed case using underscore-separated names', inject(function($rootScope, $compile) {
var element = $compile('<span ng-prop-a_bcd_e="value" />')($rootScope);
$rootScope.value = 123;
@@ -147,11 +189,11 @@ describe('ngProp*', function() {
it('should disallow property binding to onclick', inject(function($compile, $rootScope) {
// All event prop bindings are disallowed.
expect(function() {
$compile('<button ng-prop-onclick="onClickJs"></script>');
$compile('<button ng-prop-onclick="onClickJs"></button>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
expect(function() {
$compile('<button ng-prop-ONCLICK="onClickJs"></script>');
$compile('<button ng-prop-ONCLICK="onClickJs"></button>');
}).toThrowMinErr(
'$compile', 'nodomevents', 'Property bindings for HTML DOM event properties are disallowed');
}));
+13
View File
@@ -31,6 +31,19 @@ describe('urlUtils', function() {
var parsed = urlResolve('/');
expect(parsed.pathname).toBe('/');
});
it('should return an IPv6 hostname wrapped in brackets', function() {
// Support: IE 9-11 only, Edge 16-17 only (fixed in 18 Preview)
// IE/Edge don't wrap IPv6 addresses' hostnames in square brackets
// when parsed out of an anchor element.
var parsed = urlResolve('http://[::1]/');
expect(parsed.hostname).toBe('[::1]');
});
it('should not put the domain in brackets for the hostname field', function() {
var parsed = urlResolve('https://google.com/');
expect(parsed.hostname).toBe('google.com');
});
});
+1
View File
@@ -4,6 +4,7 @@
},
"globals": {
"getDomNode": false,
"helpers": false,
"mergeAnimationDetails": false,
"prepareAnimationOptions": false,
"applyAnimationStyles": false,
+2 -2
View File
@@ -1822,7 +1822,7 @@ describe('ngAnimate $animateCss', function() {
they('should not place a CSS transition block if options.skipBlocking is provided',
['enter', 'leave', 'move', 'addClass', 'removeClass'], function(event) {
inject(function($animateCss, $rootElement, $document, $window) {
inject(function($animateCss, $rootElement, $document) {
var element = angular.element('<div></div>');
$rootElement.append(element);
angular.element($document[0].body).append($rootElement);
@@ -1840,7 +1840,7 @@ describe('ngAnimate $animateCss', function() {
data.event = event;
}
var blockSpy = spyOn($window, 'blockTransitions').and.callThrough();
var blockSpy = spyOn(helpers, 'blockTransitions').and.callThrough();
data.skipBlocking = true;
var animator = $animateCss(element, data);
+53 -34
View File
@@ -20,7 +20,7 @@ describe('ngAnimateSwap', function() {
}));
it('should render a new container when the expression changes', inject(function() {
it('should render a new container when the expression changes', function() {
element = $compile('<div><div ng-animate-swap="exp">{{ exp }}</div></div>')($rootScope);
$rootScope.$digest();
@@ -40,9 +40,9 @@ describe('ngAnimateSwap', function() {
expect(third.textContent).toBe('super');
expect(third).not.toEqual(second);
expect(second.parentNode).toBeFalsy();
}));
});
it('should render a new container only when the expression property changes', inject(function() {
it('should render a new container only when the expression property changes', function() {
element = $compile('<div><div ng-animate-swap="exp.prop">{{ exp.value }}</div></div>')($rootScope);
$rootScope.exp = {
prop: 'hello',
@@ -66,9 +66,9 @@ describe('ngAnimateSwap', function() {
var three = element.find('div')[0];
expect(three.textContent).toBe('planet');
expect(three).not.toBe(two);
}));
});
it('should watch the expression as a collection', inject(function() {
it('should watch the expression as a collection', function() {
element = $compile('<div><div ng-animate-swap="exp">{{ exp.a }} {{ exp.b }} {{ exp.c }}</div></div>')($rootScope);
$rootScope.exp = {
a: 1,
@@ -99,26 +99,24 @@ describe('ngAnimateSwap', function() {
var four = element.find('div')[0];
expect(four.textContent.trim()).toBe('4');
expect(four).not.toEqual(three);
}));
they('should consider $prop as a falsy value', [false, undefined, null], function(value) {
inject(function() {
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
$rootScope.value = true;
$rootScope.$digest();
var one = element.find('div')[0];
expect(one).toBeTruthy();
$rootScope.value = value;
$rootScope.$digest();
var two = element.find('div')[0];
expect(two).toBeFalsy();
});
});
it('should consider "0" as a truthy value', inject(function() {
they('should consider $prop as a falsy value', [false, undefined, null], function(value) {
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
$rootScope.value = true;
$rootScope.$digest();
var one = element.find('div')[0];
expect(one).toBeTruthy();
$rootScope.value = value;
$rootScope.$digest();
var two = element.find('div')[0];
expect(two).toBeFalsy();
});
it('should consider "0" as a truthy value', function() {
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
$rootScope.$digest();
@@ -130,9 +128,9 @@ describe('ngAnimateSwap', function() {
var two = element.find('div')[0];
expect(two).toBeTruthy();
}));
});
it('should create a new (non-isolate) scope for each inserted clone', inject(function() {
it('should create a new (non-isolate) scope for each inserted clone', function() {
var parentScope = $rootScope.$new();
parentScope.foo = 'bar';
@@ -147,9 +145,9 @@ describe('ngAnimateSwap', function() {
expect(scopeTwo.foo).toBe('bar');
expect(scopeOne).not.toBe(scopeTwo);
}));
});
it('should destroy the previous scope when removing the element', inject(function() {
it('should destroy the previous scope when removing the element', function() {
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
$rootScope.$apply('value = 1');
@@ -166,9 +164,9 @@ describe('ngAnimateSwap', function() {
// Removing the old element (without inserting a new one).
$rootScope.$apply('value = null');
expect(scopeTwo.$$destroyed).toBe(true);
}));
});
it('should destroy the previous scope when swapping elements', inject(function() {
it('should destroy the previous scope when swapping elements', function() {
element = $compile('<div><div ng-animate-swap="value">{{ value }}</div></div>')($rootScope);
$rootScope.$apply('value = 1');
@@ -177,13 +175,34 @@ describe('ngAnimateSwap', function() {
$rootScope.$apply('value = 2');
expect(scopeOne.$$destroyed).toBe(true);
}));
});
it('should work with `ngIf` on the same element', function() {
var tmpl = '<div><div ng-animate-swap="exp" ng-if="true">{{ exp }}</div></div>';
element = $compile(tmpl)($rootScope);
$rootScope.$digest();
var first = element.find('div')[0];
expect(first).toBeFalsy();
$rootScope.exp = 'yes';
$rootScope.$digest();
var second = element.find('div')[0];
expect(second.textContent).toBe('yes');
$rootScope.exp = 'super';
$rootScope.$digest();
var third = element.find('div')[0];
expect(third.textContent).toBe('super');
expect(third).not.toEqual(second);
expect(second.parentNode).toBeFalsy();
});
describe('animations', function() {
it('should trigger a leave animation followed by an enter animation upon swap',
inject(function() {
it('should trigger a leave animation followed by an enter animation upon swap',function() {
element = $compile('<div><div ng-animate-swap="exp">{{ exp }}</div></div>')($rootScope);
$rootScope.exp = 1;
$rootScope.$digest();
@@ -208,6 +227,6 @@ describe('ngAnimateSwap', function() {
var forth = $animate.queue.shift();
expect(forth.event).toBe('leave');
expect($animate.queue.length).toBe(0);
}));
});
});
});
+109 -20
View File
@@ -954,6 +954,115 @@ describe('$aria', function() {
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
});
it('should trigger a click in browsers that provide `event.which` instead of `event.keyCode`',
function() {
compileElement(
'<section>' +
'<div ng-click="onClick($event)"></div>' +
'<ul><li ng-click="onClick($event)"></li></ul>' +
'</section>');
var divElement = element.find('div');
var liElement = element.find('li');
divElement.triggerHandler({type: 'keydown', which: 13});
liElement.triggerHandler({type: 'keydown', which: 13});
divElement.triggerHandler({type: 'keydown', which: 32});
liElement.triggerHandler({type: 'keydown', which: 32});
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
}
);
it('should not prevent default keyboard action if the target element has editable content',
inject(function($document) {
// Note:
// `contenteditable` is an enumarated (not a boolean) attribute (see
// https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/contenteditable).
// We need to check the following conditions:
// - No attribute.
// - Value: ""
// - Value: "true"
// - Value: "false"
function eventFor(keyCode) {
return {bubbles: true, cancelable: true, keyCode: keyCode};
}
compileElement(
'<section>' +
// No attribute.
'<div id="no-attribute">' +
'<div ng-click="onClick($event)"></div>' +
'<ul ng-click="onClick($event)"><li></li></ul>' +
'</div>' +
// Value: ""
'<div id="value-empty">' +
'<div ng-click="onClick($event)" contenteditable=""></div>' +
'<ul ng-click="onClick($event)"><li contenteditable=""></li></ul>' +
'</div>' +
// Value: "true"
'<div id="value-true">' +
'<div ng-click="onClick($event)" contenteditable="true"></div>' +
'<ul ng-click="onClick($event)"><li contenteditable="true"></li></ul>' +
'</div>' +
// Value: "false"
'<div id="value-false">' +
'<div ng-click="onClick($event)" contenteditable="false"></div>' +
'<ul ng-click="onClick($event)"><li contenteditable="false"></li></ul>' +
'</div>' +
'</section>');
// Support: Safari 11-12+
// Attach to DOM, because otherwise Safari will not update the `isContentEditable` property
// based on the `contenteditable` attribute.
$document.find('body').append(element);
var containers = element.children();
var container;
// Using `browserTrigger()`, because it supports event bubbling.
// No attribute | Elements are not editable.
container = containers.eq(0);
browserTrigger(container.find('div'), 'keydown', eventFor(13));
browserTrigger(container.find('ul'), 'keydown', eventFor(32));
browserTrigger(container.find('li'), 'keydown', eventFor(13));
expect(clickEvents).toEqual(['div(true)', 'ul(true)', 'li(true)']);
// Value: "" | Elements are editable.
clickEvents = [];
container = containers.eq(1);
browserTrigger(container.find('div'), 'keydown', eventFor(32));
browserTrigger(container.find('ul'), 'keydown', eventFor(13));
browserTrigger(container.find('li'), 'keydown', eventFor(32));
expect(clickEvents).toEqual(['div(false)', 'ul(true)', 'li(false)']);
// Value: "true" | Elements are editable.
clickEvents = [];
container = containers.eq(2);
browserTrigger(container.find('div'), 'keydown', eventFor(13));
browserTrigger(container.find('ul'), 'keydown', eventFor(32));
browserTrigger(container.find('li'), 'keydown', eventFor(13));
expect(clickEvents).toEqual(['div(false)', 'ul(true)', 'li(false)']);
// Value: "false" | Elements are not editable.
clickEvents = [];
container = containers.eq(3);
browserTrigger(container.find('div'), 'keydown', eventFor(32));
browserTrigger(container.find('ul'), 'keydown', eventFor(13));
browserTrigger(container.find('li'), 'keydown', eventFor(32));
expect(clickEvents).toEqual(['div(true)', 'ul(true)', 'li(true)']);
})
);
they('should not prevent default keyboard action if an interactive $type element' +
'is nested inside ng-click', nodeBlackList, function(elementType) {
function createHTML(type) {
@@ -981,26 +1090,6 @@ describe('$aria', function() {
}
);
it('should trigger a click in browsers that provide `event.which` instead of `event.keyCode`',
function() {
compileElement(
'<section>' +
'<div ng-click="onClick($event)"></div>' +
'<ul><li ng-click="onClick($event)"></li></ul>' +
'</section>');
var divElement = element.find('div');
var liElement = element.find('li');
divElement.triggerHandler({type: 'keydown', which: 13});
liElement.triggerHandler({type: 'keydown', which: 13});
divElement.triggerHandler({type: 'keydown', which: 32});
liElement.triggerHandler({type: 'keydown', which: 32});
expect(clickEvents).toEqual(['div(true)', 'li(true)', 'div(true)', 'li(true)']);
}
);
they('should not bind to key events if there is existing `ng-$prop`',
['keydown', 'keypress', 'keyup'], function(eventName) {
scope.onKeyEvent = jasmine.createSpy('onKeyEvent');
+1
View File
@@ -132,6 +132,7 @@ describe('$$cookieWriter', function() {
describe('cookie options', function() {
var fakeDocument, $$cookieWriter;
var isUndefined = angular.isUndefined;
function getLastCookieAssignment(key) {
return fakeDocument[0].cookie
+12 -4
View File
@@ -1,7 +1,9 @@
'use strict';
describe('ngMock', function() {
var noop = angular.noop;
var extend = angular.extend;
describe('TzDate', function() {
@@ -1107,11 +1109,13 @@ describe('ngMock', function() {
var mock = { log: 'module' };
beforeEach(function() {
angular.module('stringRefModule', []).service('stringRef', function() {});
module({
'service': mock,
'other': { some: 'replacement'}
},
'ngResource',
'stringRefModule',
function($provide) { $provide.value('example', 'win'); }
);
});
@@ -1130,9 +1134,9 @@ describe('ngMock', function() {
});
it('should integrate with string and function', function() {
inject(function(service, $resource, example) {
inject(function(service, stringRef, example) {
expect(service).toEqual(mock);
expect($resource).toBeDefined();
expect(stringRef).toBeDefined();
expect(example).toEqual('win');
});
});
@@ -2833,6 +2837,10 @@ describe('ngMock', function() {
describe('ngMockE2E', function() {
var noop = angular.noop;
var extend = angular.extend;
describe('$httpBackend', function() {
var hb, realHttpBackend, realHttpBackendBrowser, $http, callback;
@@ -3175,7 +3183,7 @@ describe('ngMockE2E', function() {
if (!browserSupportsCssAnimations()) return;
var element = jqLite('<div></div>');
var element = angular.element('<div></div>');
$rootElement.append(element);
// Make sure the animation has valid $animateCss options
+2
View File
@@ -1,6 +1,8 @@
'use strict';
describe('resource', function() {
var noop = angular.noop;
var extend = angular.extend;
describe('basic usage', function() {
var $resource, CreditCard, callback, $httpBackend, resourceProvider, $q;
+2 -2
View File
@@ -261,8 +261,8 @@ describe('HTML', function() {
});
});
if (!/Edge\/(16|17)/.test(window.navigator.userAgent)) {
// Skip test on Edge 16 due to browser bug.
if (!/Edge\/\d{2,}/.test(window.navigator.userAgent)) {
// Skip test on Edge due to a browser bug.
it('should throw on a form with an input named "nextSibling"', function() {
inject(function($sanitize) {
+1550 -981
View File
File diff suppressed because it is too large Load Diff