',
+ controller: function() {
+ log += 'BAR:OK';
+ }
+ }
+ });
+ module('my');
+
+ inject(function($compile, $rootScope) {
+ var fooElement = $compile('')($rootScope);
+ var barElement = $compile('')($rootScope);
+
+ expect(fooElement.find('div').text()).toEqual('FOO SUCCESS');
+ expect(barElement.find('div').text()).toEqual('BAR SUCCESS');
+ expect(log).toEqual('FOO:OKBAR:OK');
+ });
+ });
+
it('should register a directive via $compileProvider.component()', function() {
module(function($compileProvider) {
$compileProvider.component('myComponent', {
From 1991e77e43e09b55792af5ca082482d2f0cfaf05 Mon Sep 17 00:00:00 2001
From: Georgios Kalpakas
Date: Mon, 3 Jul 2017 14:21:24 +0300
Subject: [PATCH 003/552] docs(changelog): add release notes for 1.6.5
---
CHANGELOG.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 93 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 17ac2b3c83e5..35dd6553ce8a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,96 @@
+
+# 1.6.5 toffee-salinization (2017-07-03)
+
+
+## Bug Fixes
+- **core:**
+ - correctly detect Error instances from different contexts
+ ([6daca0](https://github.com/angular/angular.js/commit/6daca023e42098f7098b9bf153c8e53a17af84f1),
+ [#15868](https://github.com/angular/angular.js/issues/15868),
+ [#15872](https://github.com/angular/angular.js/issues/15872))
+ - deprecate `angular.merge`
+ ([dc41f4](https://github.com/angular/angular.js/commit/dc41f465baae9bc91418a61f446596157c530b6e),
+ [#12653](https://github.com/angular/angular.js/issues/12653),
+ [#14941](https://github.com/angular/angular.js/issues/14941),
+ [#15180](https://github.com/angular/angular.js/issues/15180),
+ [#15992](https://github.com/angular/angular.js/issues/15992),
+ [#16036](https://github.com/angular/angular.js/issues/16036))
+- **ngOptions:**
+ - re-render after empty option has been removed
+ ([510d0f](https://github.com/angular/angular.js/commit/510d0f946fa1a443ad43fa31bc9337676ef31332))
+ - allow empty option to be removed and re-added
+ ([71b4da](https://github.com/angular/angular.js/commit/71b4daa4e10b6912891927ee2a7930c604b538f8))
+ - select unknown option if unmatched model does not match empty option
+ ([17d34b](https://github.com/angular/angular.js/commit/17d34b7a983a0ef63f6cf404490385c696fb0da1))
+- **orderBy:** guarantee stable sort
+ ([e50ed4](https://github.com/angular/angular.js/commit/e50ed4da9e8177168f67da68bdf02f07da4e7bcf),
+ [#14881](https://github.com/angular/angular.js/issues/14881),
+ [#15914](https://github.com/angular/angular.js/issues/15914))
+- **$parse:**
+ - do not shallow-watch inputs to one-time intercepted expressions
+ ([6e3b5a](https://github.com/angular/angular.js/commit/6e3b5a57cd921823f3eca7200a79ac5c2ef0567a))
+ - standardize one-time literal vs non-literal and interceptors
+ ([f003d9](https://github.com/angular/angular.js/commit/f003d93a3dd052dccddef41125d9c51034ac3605))
+ - do not shallow-watch inputs when wrapped in an interceptor fn
+ ([aac562](https://github.com/angular/angular.js/commit/aac5623247a86681cbe0e1c8179617b816394c1d),
+ [#15905](https://github.com/angular/angular.js/issues/15905))
+ - always re-evaluate filters within literals when an input is an object
+ ([ec9768](https://github.com/angular/angular.js/commit/ec97686f2f4a5481cc806462313a664fc7a1c893),
+ [#15964](https://github.com/angular/angular.js/issues/15964),
+ [#15990](https://github.com/angular/angular.js/issues/15990))
+- **$sanitize:** use appropriate inert document strategy for Firefox and Safari
+ ([8f31f1](https://github.com/angular/angular.js/commit/8f31f1ff43b673a24f84422d5c13d6312b2c4d94))
+- **$timeout/$interval:** do not trigger a digest on cancel
+ ([a222d0](https://github.com/angular/angular.js/commit/a222d0b452622624dc498ef0b9d3c43647fd4fbc),
+ [#16057](https://github.com/angular/angular.js/issues/16057),
+ [#16064](https://github.com/angular/angular.js/issues/16064))
+ This change might affect the use of `$timeout.flush()` in unit tests. See the commit message for
+ more info.
+- **ngMock/$interval:** add support for zero-delay intervals in tests
+ ([a1e3f8](https://github.com/angular/angular.js/commit/a1e3f8728e0a80396f980e48f8dc68dde6721b2b),
+ [#15952](https://github.com/angular/angular.js/issues/15952),
+ [#15953](https://github.com/angular/angular.js/issues/15953))
+- **angular-loader:** do not depend on "closure" globals that may not be available
+ ([a3226d](https://github.com/angular/angular.js/commit/a3226d01fadaf145713518dc5b8022b581c34e81),
+ [#15880](https://github.com/angular/angular.js/issues/15880),
+ [#15881](https://github.com/angular/angular.js/issues/15881))
+
+
+## New Features
+- **select:** expose info about selection state in controller
+ ([0b962d](https://github.com/angular/angular.js/commit/0b962d4881e98327a91c37f7317da557aa991663),
+ [#13172](https://github.com/angular/angular.js/issues/13172),
+ [#10127](https://github.com/angular/angular.js/issues/10127))
+- **$animate:** add support for `customFilter`
+ ([ab114a](https://github.com/angular/angular.js/commit/ab114af8508bdbdb1fa5fd1e070d08818d882e28),
+ [#14891](https://github.com/angular/angular.js/issues/14891))
+- **$compile:** overload `.component()` to accept object map of components
+ ([210112](https://github.com/angular/angular.js/commit/2101126ce72308d8fc468ca2411bb9972e614f79),
+ [#14579](https://github.com/angular/angular.js/issues/14579),
+ [#16062](https://github.com/angular/angular.js/issues/16062))
+- **$log:** log all parameters in IE 9, not just the first two.
+ ([3671a4](https://github.com/angular/angular.js/commit/3671a43be43d05b00c90dfb3a3f746c013139581))
+- **ngMock:** describe unflushed http requests
+ ([d9128e](https://github.com/angular/angular.js/commit/d9128e7b2371ab2bb5169ba854b21c78baa784d2),
+ [#10596](https://github.com/angular/angular.js/issues/10596),
+ [#15928](https://github.com/angular/angular.js/issues/15928))
+
+
+## Performance Improvements
+- **ngOptions:** prevent initial options repainting
+ ([ff52b1](https://github.com/angular/angular.js/commit/ff52b188a759f2cc7ee6ee78a8c646c2354a47eb),
+ [#15801](https://github.com/angular/angular.js/issues/15801),
+ [#15812](https://github.com/angular/angular.js/issues/15812),
+ [#16071](https://github.com/angular/angular.js/issues/16071))
+- **$animate:**
+ - avoid unnecessary computations if animations are globally disabled
+ ([ce5ffb](https://github.com/angular/angular.js/commit/ce5ffbf667464bd58eae4c4af0917eb2685f1f6a),
+ [#14914](https://github.com/angular/angular.js/issues/14914))
+ - do not retrieve `className` unless `classNameFilter` is used
+ ([275978](https://github.com/angular/angular.js/commit/27597887379a1904cd86832602e286894b449a75))
+
+
+
# 1.6.4 phenomenal-footnote (2017-03-31)
From 122d89b2401fadf4f6a07a906d3d25324b7d859e Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Tue, 4 Jul 2017 12:31:38 +0200
Subject: [PATCH 004/552] docs(ngOpen): improve example, correct browser compat
note
Firefox supports details since version 49
---
src/ng/directive/attrs.js | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/ng/directive/attrs.js b/src/ng/directive/attrs.js
index ba05b9ded006..3d5fd0a9d00f 100644
--- a/src/ng/directive/attrs.js
+++ b/src/ng/directive/attrs.js
@@ -317,15 +317,20 @@
*
* ## A note about browser compatibility
*
- * Edge, Firefox, and Internet Explorer do not support the `details` element, it is
+ * Internet Explorer and Edge do not support the `details` element, it is
* recommended to use {@link ng.ngShow} and {@link ng.ngHide} instead.
*
* @example
-
+
- Show/Hide me
+ List
+
+
Apple
+
Orange
+
Durian
+
From e872f0ed36f4885d9ea35dd0ebce5cc8418f5b21 Mon Sep 17 00:00:00 2001
From: Frederik Prijck
Date: Tue, 21 Feb 2017 23:42:16 +0100
Subject: [PATCH 005/552] feat($http): allow differentiation between XHR
completion, error, abort, timeout
Previously, it wasn't possible to tell if an `$http`-initiated XMLHttpRequest
was completed normally or with an error or it was aborted or timed out.
This commit adds a new property on the `response` object (`xhrStatus`) which
allows to defferentiate between the possible statuses.
Fixes #15924
Closes #15847
---
src/ng/http.js | 18 +++++-----
src/ng/httpBackend.js | 25 ++++++++++----
src/ngMock/angular-mocks.js | 8 ++---
test/ng/httpBackendSpec.js | 57 +++++++++++++++++++++++++++++++-
test/ng/httpSpec.js | 34 +++++++++++++++++++
test/ngMock/angular-mocksSpec.js | 52 ++++++++++++++++++-----------
6 files changed, 154 insertions(+), 40 deletions(-)
diff --git a/src/ng/http.js b/src/ng/http.js
index 9262e3dec5ea..78628a0ffbf9 100644
--- a/src/ng/http.js
+++ b/src/ng/http.js
@@ -453,6 +453,7 @@ function $HttpProvider() {
* - **headers** – `{function([headerName])}` – Header getter function.
* - **config** – `{Object}` – The configuration object that was used to generate the request.
* - **statusText** – `{string}` – HTTP status text of the response.
+ * - **xhrStatus** – `{string}` – Status of the XMLHttpRequest (`complete`, `error`, `timeout` or `abort`).
*
* A response status code between 200 and 299 is considered a success status and will result in
* the success callback being called. Any response status code outside of that range is
@@ -1294,9 +1295,9 @@ function $HttpProvider() {
} else {
// serving from cache
if (isArray(cachedResp)) {
- resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3]);
+ resolvePromise(cachedResp[1], cachedResp[0], shallowCopy(cachedResp[2]), cachedResp[3], cachedResp[4]);
} else {
- resolvePromise(cachedResp, 200, {}, 'OK');
+ resolvePromise(cachedResp, 200, {}, 'OK', 'complete');
}
}
} else {
@@ -1353,10 +1354,10 @@ function $HttpProvider() {
* - resolves the raw $http promise
* - calls $apply
*/
- function done(status, response, headersString, statusText) {
+ function done(status, response, headersString, statusText, xhrStatus) {
if (cache) {
if (isSuccess(status)) {
- cache.put(url, [status, response, parseHeaders(headersString), statusText]);
+ cache.put(url, [status, response, parseHeaders(headersString), statusText, xhrStatus]);
} else {
// remove promise from the cache
cache.remove(url);
@@ -1364,7 +1365,7 @@ function $HttpProvider() {
}
function resolveHttpPromise() {
- resolvePromise(response, status, headersString, statusText);
+ resolvePromise(response, status, headersString, statusText, xhrStatus);
}
if (useApplyAsync) {
@@ -1379,7 +1380,7 @@ function $HttpProvider() {
/**
* Resolves the raw $http promise.
*/
- function resolvePromise(response, status, headers, statusText) {
+ function resolvePromise(response, status, headers, statusText, xhrStatus) {
//status: HTTP response status code, 0, -1 (aborted by timeout / promise)
status = status >= -1 ? status : 0;
@@ -1388,12 +1389,13 @@ function $HttpProvider() {
status: status,
headers: headersGetter(headers),
config: config,
- statusText: statusText
+ statusText: statusText,
+ xhrStatus: xhrStatus
});
}
function resolvePromiseWithResult(result) {
- resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText);
+ resolvePromise(result.data, result.status, shallowCopy(result.headers()), result.statusText, result.xhrStatus);
}
function removePendingReq() {
diff --git a/src/ng/httpBackend.js b/src/ng/httpBackend.js
index 501c1de86c73..7e4cb6d75680 100644
--- a/src/ng/httpBackend.js
+++ b/src/ng/httpBackend.js
@@ -64,7 +64,7 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
var jsonpDone = jsonpReq(url, callbackPath, function(status, text) {
// jsonpReq only ever sets status to 200 (OK), 404 (ERROR) or -1 (WAITING)
var response = (status === 200) && callbacks.getResponse(callbackPath);
- completeRequest(callback, status, response, '', text);
+ completeRequest(callback, status, response, '', text, 'complete');
callbacks.removeCallback(callbackPath);
});
} else {
@@ -99,18 +99,29 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
status,
response,
xhr.getAllResponseHeaders(),
- statusText);
+ statusText,
+ 'complete');
};
var requestError = function() {
// The response is always empty
// See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
- completeRequest(callback, -1, null, null, '');
+ completeRequest(callback, -1, null, null, '', 'error');
+ };
+
+ var requestAborted = function() {
+ completeRequest(callback, -1, null, null, '', 'abort');
+ };
+
+ var requestTimeout = function() {
+ // The response is always empty
+ // See https://xhr.spec.whatwg.org/#request-error-steps and https://fetch.spec.whatwg.org/#concept-network-error
+ completeRequest(callback, -1, null, null, '', 'timeout');
};
xhr.onerror = requestError;
- xhr.onabort = requestError;
- xhr.ontimeout = requestError;
+ xhr.onabort = requestAborted;
+ xhr.ontimeout = requestTimeout;
forEach(eventHandlers, function(value, key) {
xhr.addEventListener(key, value);
@@ -160,14 +171,14 @@ function createHttpBackend($browser, createXhr, $browserDefer, callbacks, rawDoc
}
}
- function completeRequest(callback, status, response, headersString, statusText) {
+ function completeRequest(callback, status, response, headersString, statusText, xhrStatus) {
// cancel timeout and subsequent timeout promise resolution
if (isDefined(timeoutId)) {
$browserDefer.cancel(timeoutId);
}
jsonpDone = xhr = null;
- callback(status, response, headersString, statusText);
+ callback(status, response, headersString, statusText, xhrStatus);
}
};
diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js
index 27b4da48b2c7..3416448b40b6 100644
--- a/src/ngMock/angular-mocks.js
+++ b/src/ngMock/angular-mocks.js
@@ -1354,8 +1354,8 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
return function() {
return angular.isNumber(status)
- ? [status, data, headers, statusText]
- : [200, status, data, headers];
+ ? [status, data, headers, statusText, 'complete']
+ : [200, status, data, headers, 'complete'];
};
}
@@ -1391,14 +1391,14 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
var response = wrapped.response(method, url, data, headers, wrapped.params(url));
xhr.$$respHeaders = response[2];
callback(copy(response[0]), copy(response[1]), xhr.getAllResponseHeaders(),
- copy(response[3] || ''));
+ copy(response[3] || ''), copy(response[4]));
}
function handleTimeout() {
for (var i = 0, ii = responses.length; i < ii; i++) {
if (responses[i] === handleResponse) {
responses.splice(i, 1);
- callback(-1, undefined, '');
+ callback(-1, undefined, '', undefined, 'timeout');
break;
}
}
diff --git a/test/ng/httpBackendSpec.js b/test/ng/httpBackendSpec.js
index fbc58072894f..adb76f60b097 100644
--- a/test/ng/httpBackendSpec.js
+++ b/test/ng/httpBackendSpec.js
@@ -174,11 +174,12 @@ describe('$httpBackend', function() {
});
it('should complete the request on timeout', function() {
- callback.and.callFake(function(status, response, headers, statusText) {
+ callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
expect(status).toBe(-1);
expect(response).toBe(null);
expect(headers).toBe(null);
expect(statusText).toBe('');
+ expect(xhrStatus).toBe('timeout');
});
$backend('GET', '/url', null, callback, {});
xhr = MockXhr.$$lastInstance;
@@ -189,6 +190,60 @@ describe('$httpBackend', function() {
expect(callback).toHaveBeenCalledOnce();
});
+ it('should complete the request on abort', function() {
+ callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
+ expect(status).toBe(-1);
+ expect(response).toBe(null);
+ expect(headers).toBe(null);
+ expect(statusText).toBe('');
+ expect(xhrStatus).toBe('abort');
+ });
+ $backend('GET', '/url', null, callback, {});
+ xhr = MockXhr.$$lastInstance;
+
+ expect(callback).not.toHaveBeenCalled();
+
+ xhr.onabort();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should complete the request on error', function() {
+ callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
+ expect(status).toBe(-1);
+ expect(response).toBe(null);
+ expect(headers).toBe(null);
+ expect(statusText).toBe('');
+ expect(xhrStatus).toBe('error');
+ });
+ $backend('GET', '/url', null, callback, {});
+ xhr = MockXhr.$$lastInstance;
+
+ expect(callback).not.toHaveBeenCalled();
+
+ xhr.onerror();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should complete the request on success', function() {
+ callback.and.callFake(function(status, response, headers, statusText, xhrStatus) {
+ expect(status).toBe(200);
+ expect(response).toBe('response');
+ expect(headers).toBe('');
+ expect(statusText).toBe('');
+ expect(xhrStatus).toBe('complete');
+ });
+ $backend('GET', '/url', null, callback, {});
+ xhr = MockXhr.$$lastInstance;
+
+ expect(callback).not.toHaveBeenCalled();
+
+ xhr.statusText = '';
+ xhr.response = 'response';
+ xhr.status = 200;
+ xhr.onload();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
it('should abort request on timeout', function() {
callback.and.callFake(function(status, response) {
expect(status).toBe(-1);
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
index 4cab7d53ac71..99bdabe599ad 100644
--- a/test/ng/httpSpec.js
+++ b/test/ng/httpSpec.js
@@ -448,6 +448,28 @@ describe('$http', function() {
expect(callback).toHaveBeenCalledOnce();
});
+ it('should pass xhrStatus in response object when a request is successful', function() {
+ $httpBackend.expect('GET', '/url').respond(200, 'SUCCESS', {}, 'OK');
+ $http({url: '/url', method: 'GET'}).then(function(response) {
+ expect(response.xhrStatus).toBe('complete');
+ callback();
+ });
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should pass xhrStatus in response object when a request fails', function() {
+ $httpBackend.expect('GET', '/url').respond(404, 'ERROR', {}, 'Not Found');
+ $http({url: '/url', method: 'GET'}).then(null, function(response) {
+ expect(response.xhrStatus).toBe('complete');
+ callback();
+ });
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
it('should pass in the response object when a request failed', function() {
$httpBackend.expect('GET', '/url').respond(543, 'bad error', {'request-id': '123'});
@@ -1623,6 +1645,17 @@ describe('$http', function() {
expect(callback).toHaveBeenCalledOnce();
}));
+ it('should cache xhrStatus as well', inject(function($rootScope) {
+ doFirstCacheRequest('GET', 201, null);
+ callback.and.callFake(function(response) {
+ expect(response.xhrStatus).toBe('complete');
+ });
+
+ $http({method: 'get', url: '/url', cache: cache}).then(callback);
+ $rootScope.$digest();
+ expect(callback).toHaveBeenCalledOnce();
+ }));
+
it('should use cache even if second request was made before the first returned', function() {
$httpBackend.expect('GET', '/url').respond(201, 'fake-response');
@@ -1788,6 +1821,7 @@ describe('$http', function() {
function(response) {
expect(response.data).toBeUndefined();
expect(response.status).toBe(-1);
+ expect(response.xhrStatus).toBe('timeout');
expect(response.headers()).toEqual(Object.create(null));
expect(response.config.url).toBe('/some');
callback();
diff --git a/test/ngMock/angular-mocksSpec.js b/test/ngMock/angular-mocksSpec.js
index 27244e9a12b3..e596bcb44445 100644
--- a/test/ngMock/angular-mocksSpec.js
+++ b/test/ngMock/angular-mocksSpec.js
@@ -1343,8 +1343,8 @@ describe('ngMock', function() {
hb.flush();
expect(callback).toHaveBeenCalledTimes(2);
- expect(callback.calls.argsFor(0)).toEqual([201, 'second', '', '']);
- expect(callback.calls.argsFor(1)).toEqual([200, 'first', '', '']);
+ expect(callback.calls.argsFor(0)).toEqual([201, 'second', '', '', 'complete']);
+ expect(callback.calls.argsFor(1)).toEqual([200, 'first', '', '', 'complete']);
});
@@ -1354,7 +1354,7 @@ describe('ngMock', function() {
hb('GET', '/url1', undefined, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK', 'complete');
});
it('should default status code to 200', function() {
@@ -1377,7 +1377,19 @@ describe('ngMock', function() {
hb('GET', '/url1', null, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'first', 'header: val', 'OK', 'complete');
+ });
+
+ it('should default xhrStatus to complete', function() {
+ callback.and.callFake(function(status, response, headers, x, xhrStatus) {
+ expect(xhrStatus).toBe('complete');
+ });
+
+ hb.expect('GET', '/url1').respond('some-data');
+ hb('GET', '/url1', null, callback);
+
+ hb.flush();
+ expect(callback).toHaveBeenCalled();
});
it('should take function', function() {
@@ -1388,7 +1400,7 @@ describe('ngMock', function() {
hb('GET', '/some?q=s', 'data', callback, {a: 'b'});
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some?q=s;data;a=b;q=s', 'Connection: keep-alive', 'Moved Permanently');
+ expect(callback).toHaveBeenCalledOnceWith(301, 'GET/some?q=s;data;a=b;q=s', 'Connection: keep-alive', 'Moved Permanently', undefined);
});
it('should decode query parameters in respond() function', function() {
@@ -1400,7 +1412,7 @@ describe('ngMock', function() {
hb('GET', '/url?query=l%E2%80%A2ng%20string%20w%2F%20spec%5Eal%20char%24&id=1234&orderBy=-name', null, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;orderBy=-name;query=l•ng string w/ spec^al char$', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;orderBy=-name;query=l•ng string w/ spec^al char$', '', '', undefined);
});
it('should include regex captures in respond() params when keys provided', function() {
@@ -1412,7 +1424,7 @@ describe('ngMock', function() {
hb('GET', '/1234/article/cool-angular-article', null, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;name=cool-angular-article', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'id=1234;name=cool-angular-article', '', '', undefined);
});
it('should default response headers to ""', function() {
@@ -1425,8 +1437,8 @@ describe('ngMock', function() {
hb.flush();
expect(callback).toHaveBeenCalledTimes(2);
- expect(callback.calls.argsFor(0)).toEqual([200, 'first', '', '']);
- expect(callback.calls.argsFor(1)).toEqual([200, 'second', '', '']);
+ expect(callback.calls.argsFor(0)).toEqual([200, 'first', '', '', 'complete']);
+ expect(callback.calls.argsFor(1)).toEqual([200, 'second', '', '', 'complete']);
});
it('should be able to override response of expect definition', function() {
@@ -1436,7 +1448,7 @@ describe('ngMock', function() {
hb('GET', '/url1', null, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
});
it('should be able to override response of when definition', function() {
@@ -1446,7 +1458,7 @@ describe('ngMock', function() {
hb('GET', '/url1', null, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
});
it('should be able to override response of expect definition with chaining', function() {
@@ -1455,7 +1467,7 @@ describe('ngMock', function() {
hb('GET', '/url1', null, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
});
it('should be able to override response of when definition with chaining', function() {
@@ -1464,7 +1476,7 @@ describe('ngMock', function() {
hb('GET', '/url1', null, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'second', '', '', 'complete');
});
});
@@ -1657,7 +1669,7 @@ describe('ngMock', function() {
canceler(); // simulate promise resolution
- expect(callback).toHaveBeenCalledWith(-1, undefined, '');
+ expect(callback).toHaveBeenCalledWith(-1, undefined, '', undefined, 'timeout');
hb.verifyNoOutstandingExpectation();
hb.verifyNoOutstandingRequest();
});
@@ -1669,7 +1681,7 @@ describe('ngMock', function() {
hb('GET', '/url1', null, callback, null, 200);
$timeout.flush(300);
- expect(callback).toHaveBeenCalledWith(-1, undefined, '');
+ expect(callback).toHaveBeenCalledWith(-1, undefined, '', undefined, 'timeout');
hb.verifyNoOutstandingExpectation();
hb.verifyNoOutstandingRequest();
}));
@@ -1831,7 +1843,7 @@ describe('ngMock', function() {
hb[shortcut]('/foo').respond('bar');
hb(method, '/foo', undefined, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'bar', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'bar', '', '', 'complete');
});
});
});
@@ -1846,7 +1858,7 @@ describe('ngMock', function() {
hb[routeShortcut](this, '/route').respond('path');
hb(this, '/route', undefined, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
}
);
they('should match colon delimited parameters in ' + routeShortcut + ' $prop method', methods,
@@ -1854,7 +1866,7 @@ describe('ngMock', function() {
hb[routeShortcut](this, '/route/:id/path/:s_id').respond('path');
hb(this, '/route/123/path/456', undefined, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
}
);
they('should ignore query param when matching in ' + routeShortcut + ' $prop method', methods,
@@ -1862,7 +1874,7 @@ describe('ngMock', function() {
hb[routeShortcut](this, '/route/:id').respond('path');
hb(this, '/route/123?q=str&foo=bar', undefined, callback);
hb.flush();
- expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'path', '', '', 'complete');
}
);
});
@@ -2462,7 +2474,7 @@ describe('ngMockE2E', function() {
$browser.defer.flush();
expect(realHttpBackend).not.toHaveBeenCalled();
- expect(callback).toHaveBeenCalledOnceWith(200, 'passThrough override', '', '');
+ expect(callback).toHaveBeenCalledOnceWith(200, 'passThrough override', '', '', 'complete');
}));
it('should pass through to an httpBackend that uses the same $browser service', inject(function($browser) {
From af83c1541cf5fbe97e1f15d891977975af003020 Mon Sep 17 00:00:00 2001
From: "z.ky"
Date: Thu, 6 Jul 2017 18:06:34 +0100
Subject: [PATCH 006/552] docs(ngRepeat): correct typo
Closes #16088
---
src/ng/directive/ngRepeat.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ng/directive/ngRepeat.js b/src/ng/directive/ngRepeat.js
index c0a1794b9147..ea32162a0c9c 100644
--- a/src/ng/directive/ngRepeat.js
+++ b/src/ng/directive/ngRepeat.js
@@ -216,7 +216,7 @@
* mapped to the same DOM element, which is not possible.)
*
*
- * Note: the `tracky by` expression must come last - after any filters, and the alias expression.
+ * Note: the `track by` expression must come last - after any filters, and the alias expression.
*
*
* For example: `item in items` is equivalent to `item in items track by $id(item)`. This implies that the DOM elements
From 77b302ab00b28294d31948cfcb702219b84c9227 Mon Sep 17 00:00:00 2001
From: Eyal Ronel
Date: Mon, 10 Jul 2017 17:53:37 -0400
Subject: [PATCH 007/552] docs($resource): add missing closing square bracket
in example
Closes #16090
---
src/ngResource/resource.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js
index c1d6ec8a818b..054fb2523ac6 100644
--- a/src/ngResource/resource.js
+++ b/src/ngResource/resource.js
@@ -488,7 +488,7 @@ angular.module('ngResource', ['ng']).
* $resourceProvider.defaults.actions.update = {
* method: 'PUT'
* };
- * });
+ * }]);
* ```
*
* Or you can even overwrite the whole `actions` list and specify your own:
From c8793431b600338edc382db1ce0020b5c9d5382f Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Tue, 11 Jul 2017 15:34:46 +0200
Subject: [PATCH 008/552] docs($rootScope.Scope): improve wording in $watch
Closes #16050
---
src/ng/rootScope.js | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/ng/rootScope.js b/src/ng/rootScope.js
index 27f8ab992aec..bb7f83b42462 100644
--- a/src/ng/rootScope.js
+++ b/src/ng/rootScope.js
@@ -297,8 +297,8 @@ function $RootScopeProvider() {
* according to the {@link angular.equals} function. To save the value of the object for
* later comparison, the {@link angular.copy} function is used. This therefore means that
* watching complex objects will have adverse memory and performance implications.
- * - This should not be used to watch for changes in objects that are
- * or contain [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}.
+ * - This should not be used to watch for changes in objects that are (or contain)
+ * [File](https://developer.mozilla.org/docs/Web/API/File) objects due to limitations with {@link angular.copy `angular.copy`}.
* - The watch `listener` may change the model, which may trigger other `listener`s to fire.
* This is achieved by rerunning the watchers until no changes are detected. The rerun
* iteration limit is 10 to prevent an infinite loop deadlock.
From 7f2accaa3aed18e811338c9593fb363808c2b40d Mon Sep 17 00:00:00 2001
From: Nikos Katsos
Date: Thu, 29 Jun 2017 14:30:53 +0300
Subject: [PATCH 009/552] fix($http): do not throw error if `Content-Type` is
not `application/json` but response is JSON-like
Previously, when the response data was JSON-like, `$http` would try to
`JSON.parse` them and throw if they were not actually JSON. This happened even
if the `Content-Type` header was not `application/json`. As a result, it was not
possible to send `text/plain` data that looked like JSON (e.g. `{abcd}`).
The reason for not relying solely on the `Content-Type` is that many users serve
JSON data without proper headers.
This commit fixes it by returning the raw response text if `$http` fails to
parse a JSON-like response, unless the `Content-Type` header has been explicitly
set to `application/json` (in which case it will still fail with an error).
Fixes #16027
Closes #16075
---
src/ng/http.js | 7 ++++++-
test/ng/httpSpec.js | 39 ++++++++++++++++++++++++++++++++++++++-
2 files changed, 44 insertions(+), 2 deletions(-)
diff --git a/src/ng/http.js b/src/ng/http.js
index 78628a0ffbf9..f2c597c452a7 100644
--- a/src/ng/http.js
+++ b/src/ng/http.js
@@ -137,10 +137,15 @@ function defaultHttpResponseTransform(data, headers) {
if (tempData) {
var contentType = headers('Content-Type');
- if ((contentType && (contentType.indexOf(APPLICATION_JSON) === 0)) || isJsonLike(tempData)) {
+ var hasJsonContentType = contentType && (contentType.indexOf(APPLICATION_JSON) === 0);
+
+ if (hasJsonContentType || isJsonLike(tempData)) {
try {
data = fromJson(tempData);
} catch (e) {
+ if (!hasJsonContentType) {
+ return data;
+ }
throw $httpMinErr('baddata', 'Data must be a valid JSON object. Received: "{0}". ' +
'Parse error: "{1}"', data, e);
}
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
index 99bdabe599ad..5b21ee15a56b 100644
--- a/test/ng/httpSpec.js
+++ b/test/ng/httpSpec.js
@@ -1402,8 +1402,45 @@ describe('$http', function() {
expect(errCallback.calls.mostRecent().args[0]).toEqualMinErr('$http', 'baddata');
});
- });
+ it('should not throw an error if JSON is invalid but content-type is not application/json', function() {
+ $httpBackend.expect('GET', '/url').respond('{abcd}', {'Content-Type': 'text/plain'});
+
+ $http.get('/url').then(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+
+ it('should not throw an error if JSON is invalid but content-type is not specified', function() {
+ $httpBackend.expect('GET', '/url').respond('{abcd}');
+
+ $http.get('/url').then(callback);
+ $httpBackend.flush();
+
+ expect(callback).toHaveBeenCalledOnce();
+ });
+ it('should return response unprocessed if JSON is invalid but content-type is not application/json', function() {
+ var response = '{abcd}';
+ $httpBackend.expect('GET', '/url').respond(response, {'Content-Type': 'text/plain'});
+
+ $http.get('/url').then(callback);
+ $httpBackend.flush();
+
+ expect(callback.calls.mostRecent().args[0].data).toBe(response);
+ });
+
+ it('should return response unprocessed if JSON is invalid but content-type is not specified', function() {
+ var response = '{abcd}';
+ $httpBackend.expect('GET', '/url').respond(response);
+
+ $http.get('/url').then(callback);
+ $httpBackend.flush();
+
+ expect(callback.calls.mostRecent().args[0].data).toBe(response);
+ });
+
+ });
it('should have access to response headers', function() {
$httpBackend.expect('GET', '/url').respond(200, 'response', {h1: 'header1'});
From 25bf1ef07cc76116ce77a5727c97c7597bfc480b Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Wed, 12 Jul 2017 11:15:09 +0200
Subject: [PATCH 010/552] chore(doc-gen): insert current tagged version if
missing from list of all versions
In commit ce49edc08b3d642f3768f4282d391062d2f83037, we switched to npm info (now yarn info)
instead of the local git repository information to get the list of currently available versions for
the docs app. This means that during a release the version that is currently tagged is not yet
available on npm, and therefore our list of available versions is incomplete.
We now simply add the current build version (read from build/version.json) to the list of all
versions if it fulfills the following conditions:
- it is not a snapshot build
- it is not already part of the list of all versions (i.e. if you are building locally on a tagged commit)
Closes #15741
Closes #16099
---
docs/config/processors/versions-data.js | 9 +++++++++
1 file changed, 9 insertions(+)
diff --git a/docs/config/processors/versions-data.js b/docs/config/processors/versions-data.js
index 4e0d3aeffb47..9856ef75682b 100644
--- a/docs/config/processors/versions-data.js
+++ b/docs/config/processors/versions-data.js
@@ -47,6 +47,14 @@ module.exports = function generateVersionDocProcessor(gitData) {
var latestMap = {};
+ // When the docs are built on a tagged commit, yarn info won't include the latest release,
+ // so we add it manually based on the local version.json file.
+ var missesCurrentVersion = !currentVersion.isSnapshot && !versions.find(function(version) {
+ return version === currentVersion.version;
+ });
+
+ if (missesCurrentVersion) versions.push(currentVersion.version);
+
versions = versions
.filter(function(versionStr) {
return blacklist.indexOf(versionStr) === -1;
@@ -70,6 +78,7 @@ module.exports = function generateVersionDocProcessor(gitData) {
})
.reverse();
+ // List the latest version for each branch
var latest = sortObject(latestMap, reverse(semver.compare))
.map(function(version) { return makeOption(version, 'Latest'); });
From 0616dde95efbd16d541ce2f27fc5a870b704230c Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Wed, 12 Jul 2017 12:02:18 +0200
Subject: [PATCH 011/552] chore(travis): add commitplease validation to
ci-checks
This will provide feedback to contributors without getting in the way of writing invalid commit messages locally.
The git hook integration is turned off.
Committers who push directly to the repo can be expected to use correct commit messages ;) Most changes go through PRs anyway.
Note that "Merge commit" messages and everything starting with "WIP" is always allowed by commitplease. Follow issue https://github.com/jzaefferer/commitplease/issues/101 for more info.
Related to #14888
Closes #16097
---
package.json | 7 ++++++-
scripts/travis/build.sh | 7 +++++++
yarn.lock | 20 ++++++++++++++++++++
3 files changed, 33 insertions(+), 1 deletion(-)
diff --git a/package.json b/package.json
index 8c710e67fc7d..767716f9f0db 100644
--- a/package.json
+++ b/package.json
@@ -30,6 +30,7 @@
"changez-angular": "^2.1.2",
"cheerio": "^0.17.0",
"commitizen": "^2.3.0",
+ "commitplease": "^2.7.10",
"cross-spawn": "^4.0.0",
"cz-conventional-changelog": "1.1.4",
"dgeni": "^0.4.0",
@@ -95,9 +96,13 @@
"stringmap": "^0.2.2"
},
"dependencies": {},
+ "commitplease": {
+ "style": "angular",
+ "nohook": true
+ },
"config": {
"commitizen": {
"path": "node_modules/cz-conventional-changelog"
}
}
-}
+}
\ No newline at end of file
diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh
index 0ecfc25daf5f..bf9be1724550 100755
--- a/scripts/travis/build.sh
+++ b/scripts/travis/build.sh
@@ -7,6 +7,13 @@ export SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev`
if [ "$JOB" == "ci-checks" ]; then
grunt ci-checks
+ if [[ $TRAVIS_PULL_REQUEST != 'false' ]]; then
+ # validate commit messages of all commits in the PR
+ # 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/.../..}"
+ fi
elif [ "$JOB" == "unit" ]; then
if [ "$BROWSER_PROVIDER" == "browserstack" ]; then
BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9"
diff --git a/yarn.lock b/yarn.lock
index 46d99a35381b..54e17f12a7db 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1166,6 +1166,16 @@ commitizen@^2.3.0:
shelljs "0.7.5"
strip-json-comments "2.0.1"
+commitplease@^2.7.10:
+ version "2.7.10"
+ resolved "https://registry.yarnpkg.com/commitplease/-/commitplease-2.7.10.tgz#129af5abb365b46f25e652020c5d1548c947f163"
+ dependencies:
+ chalk "^1.1.1"
+ git-tools "^0.2.1"
+ ini "^1.3.4"
+ object-assign "^4.1.0"
+ semver "^5.1.0"
+
commondir@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/commondir/-/commondir-0.0.1.tgz#89f00fdcd51b519c578733fec563e6a6da7f5be2"
@@ -2475,6 +2485,12 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
+git-tools@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.yarnpkg.com/git-tools/-/git-tools-0.2.1.tgz#6e1846af2c0e91ab59258b48f9b53c1279b3b273"
+ dependencies:
+ spawnback "~1.0.0"
+
glob-base@^0.3.0:
version "0.3.0"
resolved "https://registry.yarnpkg.com/glob-base/-/glob-base-0.3.0.tgz#dbb164f6221b1c0b1ccf82aea328b497df0ea3c4"
@@ -5912,6 +5928,10 @@ spawn-sync@^1.0.15:
concat-stream "^1.4.7"
os-shim "^0.1.2"
+spawnback@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/spawnback/-/spawnback-1.0.0.tgz#f73662f7e54d95367eca74d6426c677dd7ea686f"
+
spdx-correct@~1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40"
From 6a22c79a94087bd9f61e84b7c11e26dc83a9bb07 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Thu, 13 Jul 2017 10:38:57 +0200
Subject: [PATCH 012/552] chore(travis): fix bower install
We removed `grunt package` from JOB_UNIT in 4015e0fde5bb0005d9812bdd1fb5c6496f5b1944,
but this runs `grunt bower` which JOB_UNIT needs.
This commit adds `grunt bower` to JOB_UNIT.
Closes #16105
---
scripts/travis/before_build.sh | 2 ++
1 file changed, 2 insertions(+)
diff --git a/scripts/travis/before_build.sh b/scripts/travis/before_build.sh
index 76ff7193520f..df9e78fbb8e6 100755
--- a/scripts/travis/before_build.sh
+++ b/scripts/travis/before_build.sh
@@ -18,6 +18,8 @@ fi
# unit runs the docs tests too which need a built version of the code
if [ "$JOB" = "unit" ]; then
+ grunt bower
+ grunt validate-angular-files
grunt build
fi
From 7e97db93ff3ccd45ba943d2df28d4b53c0efc280 Mon Sep 17 00:00:00 2001
From: SteveAndrewArcher
Date: Thu, 13 Jul 2017 04:13:09 -0500
Subject: [PATCH 013/552] docs(input[week]): clarify value of Date object
Add a note to documentation of input[week] to explicitly state that the
resulting Date object's value is set to Thursday at midnight of the
specified week.
Resolves #15883
Closes #16104
---
src/ng/directive/input.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 182690a12310..73f6ef898c51 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -457,6 +457,10 @@ var inputType = {
* The model must always be a Date object, otherwise AngularJS will throw an error.
* Invalid `Date` objects (dates whose `getTime()` is `NaN`) will be rendered as an empty string.
*
+ * The value of the resulting Date object will be set to Thursday at 00:00:00 of the requested week,
+ * due to ISO-8601 week numbering standards. Information on ISO's system for numbering the weeks of the
+ * year can be found at: https://en.wikipedia.org/wiki/ISO_8601#Week_dates
+ *
* The timezone to be used to read/write the `Date` instance in the model can be defined using
* {@link ng.directive:ngModelOptions ngModelOptions}. By default, this is the timezone of the browser.
*
From 828a2757b2e04a3bec12012c127812559fab78e3 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Thu, 13 Jul 2017 11:14:55 +0200
Subject: [PATCH 014/552] chore(*): deploy (docs|code) .angularjs.org to
Firebase via Travis
- code.angularjs.org and docs.angularjs.org are two separate Firebase projects
- both are automatically deployed via Travis config
- Travis is split up into 2 build stages: first, all tests are run, and if they pass, the deploy
stage runs a single job with both deployments (actual deployment depends on the state of the commit)
- docs. is deployed directly to Firebase hosting
- code. is uploaded to Firebase Google Cloud Storage and uses Firebase hosting rewrites to acces the
files
- jenkins builds still push the code builds to the code.angularjs.org Github repository
Closes #9674
Closes #16093
---
.firebaserc | 5 ++
.gitignore | 2 +-
.travis.yml | 66 +++++++++++---
Gruntfile.js | 14 ++-
firebase.json | 24 +++++
readme.firebase.docs.md | 10 +++
.../.eslintrc.json | 5 ++
.../code.angularjs.org-firebase/.firebaserc | 5 ++
.../code.angularjs.org-firebase/firebase.json | 21 +++++
.../functions/index.js | 75 ++++++++++++++++
.../functions/package.json | 10 +++
.../public/favicon.ico | Bin 0 -> 1150 bytes
.../public/googleb96cceae5888d79f.html | 1 +
.../public/index.html | 10 +++
.../public/robots.txt | 5 ++
.../readme.firebase.code.md | 12 +++
.../code.angularjs.org-firebase/storage.rules | 7 ++
scripts/code.angularjs.org/publish.sh | 17 +---
scripts/travis/build.sh | 84 ++++++++++--------
19 files changed, 308 insertions(+), 65 deletions(-)
create mode 100644 .firebaserc
create mode 100644 firebase.json
create mode 100644 readme.firebase.docs.md
create mode 100644 scripts/code.angularjs.org-firebase/.eslintrc.json
create mode 100644 scripts/code.angularjs.org-firebase/.firebaserc
create mode 100644 scripts/code.angularjs.org-firebase/firebase.json
create mode 100644 scripts/code.angularjs.org-firebase/functions/index.js
create mode 100644 scripts/code.angularjs.org-firebase/functions/package.json
create mode 100644 scripts/code.angularjs.org-firebase/public/favicon.ico
create mode 100644 scripts/code.angularjs.org-firebase/public/googleb96cceae5888d79f.html
create mode 100644 scripts/code.angularjs.org-firebase/public/index.html
create mode 100644 scripts/code.angularjs.org-firebase/public/robots.txt
create mode 100644 scripts/code.angularjs.org-firebase/readme.firebase.code.md
create mode 100644 scripts/code.angularjs.org-firebase/storage.rules
diff --git a/.firebaserc b/.firebaserc
new file mode 100644
index 000000000000..8691a8f11929
--- /dev/null
+++ b/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "docs-angularjs-org-9p2"
+ }
+}
diff --git a/.gitignore b/.gitignore
index e897180b89d1..42c5e13b4421 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,7 +9,7 @@ performance/temp*.html
*~
*.swp
angular.js.tmproj
-/node_modules/
+node_modules/
bower_components/
angular.xcodeproj
.idea
diff --git a/.travis.yml b/.travis.yml
index 478de09a5484..617083342a15 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -10,7 +10,7 @@ cache:
branches:
except:
- - /^g3_.*$/
+ - "/^g3_.*$/"
env:
matrix:
@@ -20,14 +20,15 @@ env:
- JOB=e2e TEST_TARGET=jqlite BROWSER_PROVIDER=saucelabs
- JOB=e2e TEST_TARGET=jquery BROWSER_PROVIDER=saucelabs
global:
- - CXX=g++-4.8 # node 4 likes the G++ v4.8 compiler
+ # node 4 likes the G++ v4.8 compiler
+ # see https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Node.js-v4-(or-io.js-v3)-compiler-requirements
+ - CXX=g++-4.8
- SAUCE_USERNAME=angular-ci
- SAUCE_ACCESS_KEY=9b988f434ff8-fbca-8aa4-4ae3-35442987
- LOGS_DIR=/tmp/angular-build/logs
- BROWSER_PROVIDER_READY_FILE=/tmp/browsersprovider-tunnel-ready
+ - secure: oTBjhnOKhs0qDSKTf7fE4f6DYiNDPycvB7qfSF5QRIbJK/LK/J4UtFwetXuXj79HhUZG9qnoT+5e7lPaiaMlpsIKn9ann7ffqFWN1E8TMtpJF+AGigx3djYElwfgf5nEnFUFhwjFzvbfpZNnxVGgX5YbIZpe/WUbHkP4ffU0Wks=
-# node 4 likes the G++ v4.8 compiler
-# see https://docs.travis-ci.com/user/languages/javascript-with-nodejs#Node.js-v4-(or-io.js-v3)-compiler-requirements
addons:
apt:
sources:
@@ -37,20 +38,61 @@ addons:
before_script:
- du -sh ./node_modules ./bower_components/ || true
- - ./scripts/travis/before_build.sh
-
+ - "./scripts/travis/before_build.sh"
script:
- - ./scripts/travis/build.sh
+ - "./scripts/travis/build.sh"
after_script:
- - ./scripts/travis/tear_down_browser_provider.sh
- - ./scripts/travis/print_logs.sh
+ - "./scripts/travis/tear_down_browser_provider.sh"
+ - "./scripts/travis/print_logs.sh"
notifications:
webhooks:
urls:
- https://webhooks.gitter.im/e/d2120f3f2bb39a4531b2
- http://104.197.9.155:8484/hubot/travis/activity #hubot-server
- on_success: always # options: [always|never|change] default: always
- on_failure: always # options: [always|never|change] default: always
- on_start: always # default: false
+ on_success: always # options: [always|never|change] default: always
+ on_failure: always # options: [always|never|change] default: always
+ on_start: always # default: false
+
+jobs:
+ include:
+ - stage: deploy
+ env:
+ - JOB=deploy
+ before_script: skip
+ script:
+ - "./scripts/travis/build.sh"
+ # Work around the 10min Travis timeout so the code.angularjs firebase+gcs code deploy can complete
+ before_deploy: |
+ function keep_alive() {
+ while true; do
+ echo -en "\a"
+ sleep 5
+ done
+ }
+ keep_alive &
+ deploy:
+ - provider: firebase
+ skip_cleanup: true
+ token:
+ secure: $FIREBASE_TOKEN
+ on:
+ repo: angular/angular.js
+ all_branches: true
+ # deploy a new docs version when the commit is tagged on the "latest" npm version
+ condition: $TRAVIS_TAG != '' && $( jq ".distTag" "package.json" | tr -d "\"[:space:]" ) = latest
+ - provider: gcs
+ skip_cleanup: true
+ access_key_id: GOOGLDB7W2J3LFHICF3R
+ secret_access_key:
+ secure: tHIFdSq55qkyZf9zT/3+VkhUrTvOTMuswxXU3KyWaBrSieZqG0UnUDyNm+n3lSfX95zEl/+rJAWbfvhVSxZi13ndOtvRF+MdI1cvow2JynP0aDSiPffEvVrZOmihD6mt2SlMfhskr5FTduQ69kZG6DfLcve1PPDaIwnbOv3phb8=
+ bucket: code-angularjs-org-338b8.appspot.com
+ local-dir: upload
+ detect_encoding: true # detects gzip compression
+ on:
+ repo: angular/angular.js
+ all_branches: true
+ # upload the build when the commit is tagged or the branch is "master"
+ condition: $TRAVIS_TAG != '' || ($TRAVIS_PULL_REQUEST = false && $TRAVIS_BRANCH = master)
+
diff --git a/Gruntfile.js b/Gruntfile.js
index 5f602bed8fd7..8de52bca501f 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -49,7 +49,6 @@ if (!process.env.TRAVIS && !process.env.JENKINS_HOME) {
}
}
-
module.exports = function(grunt) {
// this loads all the node_modules that start with `grunt-` as plugins
@@ -64,6 +63,8 @@ module.exports = function(grunt) {
NG_VERSION.cdn = versionInfo.cdnVersion;
var dist = 'angular-' + NG_VERSION.full;
+ var deployVersion = NG_VERSION.isSnapshot ? 'snapshot' : NG_VERSION.full;
+
if (versionInfo.cdnVersion == null) {
throw new Error('Unable to read CDN version, are you offline or has the CDN not been properly pushed?\n' +
'Perhaps you want to set the NG1_BUILD_NO_REMOTE_VERSION_REQUESTS environment variable?');
@@ -324,6 +325,15 @@ module.exports = function(grunt) {
expand: true,
dot: true,
dest: dist + '/'
+ },
+ firebaseCodeDeploy: {
+ options: {
+ mode: 'gzip'
+ },
+ src: ['**'],
+ cwd: 'build',
+ expand: true,
+ dest: 'upload/' + deployVersion + '/'
}
},
@@ -418,7 +428,7 @@ module.exports = function(grunt) {
'write',
'docs',
'copy',
- 'compress'
+ 'compress:build'
]);
grunt.registerTask('ci-checks', [
'ddescribe-iit',
diff --git a/firebase.json b/firebase.json
new file mode 100644
index 000000000000..3427962f39bd
--- /dev/null
+++ b/firebase.json
@@ -0,0 +1,24 @@
+{
+ "hosting": {
+ "public": "build/docs",
+ "ignore": [
+ "/index.html",
+ "/index-debug.html",
+ "/index-jquery.html"
+ ],
+ "rewrites": [
+ {
+ "source": "/",
+ "destination": "/index-production.html"
+ },
+ {
+ "source": "/index.html",
+ "destination": "/index-production.html"
+ },
+ {
+ "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.json|.css|.svg|.ttf|.woff|.woff2|.eot)",
+ "destination": "/index-production.html"
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/readme.firebase.docs.md b/readme.firebase.docs.md
new file mode 100644
index 000000000000..d73e9a74f5d2
--- /dev/null
+++ b/readme.firebase.docs.md
@@ -0,0 +1,10 @@
+Firebase for docs.angularjs.org
+===============================
+
+The docs are deployed to Google Firebase hosting via Travis deployment config, which expects
+firebase.json and .firebaserc in the repository root.
+
+See travis.yml for the complete deployment config.
+
+See /scripts/code.angularjs.org-firebase/readme.firebase.code.md for the firebase deployment to
+code.angularjs.org
\ No newline at end of file
diff --git a/scripts/code.angularjs.org-firebase/.eslintrc.json b/scripts/code.angularjs.org-firebase/.eslintrc.json
new file mode 100644
index 000000000000..22abc7fb2b7a
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/.eslintrc.json
@@ -0,0 +1,5 @@
+{
+ "env": {
+ "es6": true
+ }
+}
diff --git a/scripts/code.angularjs.org-firebase/.firebaserc b/scripts/code.angularjs.org-firebase/.firebaserc
new file mode 100644
index 000000000000..5ae9ae1e0f91
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/.firebaserc
@@ -0,0 +1,5 @@
+{
+ "projects": {
+ "default": "code-angularjs-org-338b8"
+ }
+}
diff --git a/scripts/code.angularjs.org-firebase/firebase.json b/scripts/code.angularjs.org-firebase/firebase.json
new file mode 100644
index 000000000000..a4d299f9a105
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/firebase.json
@@ -0,0 +1,21 @@
+{
+ "hosting": {
+ "public": "public",
+ "redirects": [
+ {
+ "source": "/:version/docs",
+ "destination": "/:version/docs/index.html",
+ "type": 301
+ }
+ ],
+ "rewrites": [
+ {
+ "source": "/**",
+ "function": "sendStoredFile"
+ }
+ ]
+ },
+ "storage": {
+ "rules": "storage.rules"
+ }
+}
diff --git a/scripts/code.angularjs.org-firebase/functions/index.js b/scripts/code.angularjs.org-firebase/functions/index.js
new file mode 100644
index 000000000000..18fdd6c7a44a
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/functions/index.js
@@ -0,0 +1,75 @@
+'use strict';
+
+const functions = require('firebase-functions');
+const gcs = require('@google-cloud/storage')();
+const path = require('path');
+
+const gcsBucketId = `${process.env.GCLOUD_PROJECT}.appspot.com`;
+const LOCAL_TMP_FOLDER = '/tmp/';
+
+const BROWSER_CACHE_DURATION = 300;
+const CDN_CACHE_DURATION = 600;
+
+function sendStoredFile(request, response) {
+ let filePathSegments = request.path.split('/').filter((segment) => {
+ // Remove empty leading or trailing path parts
+ return segment !== '';
+ });
+
+ const version = filePathSegments[0];
+ const isDocsPath = filePathSegments[1] === 'docs';
+ const lastSegment = filePathSegments[filePathSegments.length - 1];
+ const bucket = gcs.bucket(gcsBucketId);
+
+ let downloadSource;
+ let downloadDestination;
+ let fileName;
+
+ if (isDocsPath && filePathSegments.length === 2) {
+ fileName = 'index.html';
+ filePathSegments = [version, 'docs', fileName];
+ } else {
+ fileName = lastSegment;
+ }
+
+ downloadSource = path.join.apply(null, filePathSegments);
+ downloadDestination = `${LOCAL_TMP_FOLDER}${fileName}`;
+
+ downloadAndSend(downloadSource, downloadDestination).catch(error => {
+ if (isDocsPath && error.code === 404) {
+ fileName = 'index.html';
+ filePathSegments = [version, 'docs', fileName];
+ downloadSource = path.join.apply(null, filePathSegments);
+ downloadDestination = `${LOCAL_TMP_FOLDER}${fileName}`;
+
+ return downloadAndSend(downloadSource, downloadDestination);
+ }
+
+ return Promise.reject(error);
+ }).catch(error => {
+ let message = 'General error';
+ if (error.code === 404) {
+ if (fileName.split('.').length === 1) {
+ message = 'Directory listing is not supported';
+ } else {
+ message = 'File not found';
+ }
+ }
+
+ return response.status(error.code).send(message);
+ });
+
+ function downloadAndSend(downloadSource, downloadDestination) {
+ return bucket.file(downloadSource).download({
+ destination: downloadDestination
+ }).then(() => {
+ return response.status(200)
+ .set({
+ 'Cache-Control': `public, max-age=${BROWSER_CACHE_DURATION}, s-maxage=${CDN_CACHE_DURATION}`
+ })
+ .sendFile(downloadDestination);
+ });
+ }
+}
+
+exports.sendStoredFile = functions.https.onRequest(sendStoredFile);
diff --git a/scripts/code.angularjs.org-firebase/functions/package.json b/scripts/code.angularjs.org-firebase/functions/package.json
new file mode 100644
index 000000000000..71a68bd6d34a
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/functions/package.json
@@ -0,0 +1,10 @@
+{
+ "name": "functions-firebase-code.angularjs.org",
+ "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"
+ },
+ "private": true
+}
diff --git a/scripts/code.angularjs.org-firebase/public/favicon.ico b/scripts/code.angularjs.org-firebase/public/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..fe24a63a6ba4c4b4fb0e960abb60e51dc9097d43
GIT binary patch
literal 1150
zcmZuxZAep57(QaqUkSemBP}6IGixM-R$2;?eQEiH)}M%sFz|;4MNX|WiYN&)b(!;H
z6Y*nt*TibqTMgRG)HS!ek&a*=1h9&ehx^|5d7kq==bVdB5WYh~
z5Pn9Zl1&JOBZT5`;1tgM&*K3h{B8XO04Pv69FAiQqf%+L-x}R+cT}L7K)Hp5g#({H
zs)ujg7~xW4RgiVK6Q0-0+&Z1^>C)0tWFQR5EiNv`j*gB=t1D>k=&rAj6sm=NVV@x`
zw2hCPj4lwp82qEx>mRPHtc3aV7>%aNIw8HBkub*N2zEW5@azt2gMusxh+8R_*=)Y<
z8=bRvJutLfX4G3b|p{YRzFR?!H&x@}p?E$;po?|kZ?qnve^}WB-
z4mRsN9Ew)M*>nl7U3gmR!xE0m1>G<`JzX|FKGB*SHR01IKAcZw@!XtnuR;$E^#jn_
zB7;5QtJ(ygeY^|so_h9fSglM7om_(_976-Yz&vAx!9fa&&aBlY_#=rOpwVb3X$NB#
zL~g_vWg3C__e!);o8Sd8VxX1ul+kDuzL7I*^-Y>1J_;gMx#WE_a1pONsV9-NO$>#n
zBv32t<%SgRXIPfK>)nHR-y~wL8ac%ns>*5ZLYf+~Q@=oRY&%@ak;0?
+
+
+
+
+ AngularJS
+
+
+
+
diff --git a/scripts/code.angularjs.org-firebase/public/robots.txt b/scripts/code.angularjs.org-firebase/public/robots.txt
new file mode 100644
index 000000000000..480082428fa1
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/public/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+
+Disallow: /*docs/
+Disallow: /*i18n/
+Disallow: /*.zip$
diff --git a/scripts/code.angularjs.org-firebase/readme.firebase.code.md b/scripts/code.angularjs.org-firebase/readme.firebase.code.md
new file mode 100644
index 000000000000..58a5d25835d4
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/readme.firebase.code.md
@@ -0,0 +1,12 @@
+Firebase for code.angularjs.org
+===============================
+
+This folder contains the Google Firebase scripts for the code.angularjs.org setup.
+
+firebase.json contains the rewrite rules that route every subdirectory request to the cloud function
+in functions/index.js that serves the docs from the Firebase Google Cloud Storage bucket.
+
+The deployment to the Google Cloud Storage bucket happens automatically via Travis. See the travis.yml
+file in the repository root.
+
+See /readme.firebase.docs.md for the firebase deployment to docs.angularjs.org
\ No newline at end of file
diff --git a/scripts/code.angularjs.org-firebase/storage.rules b/scripts/code.angularjs.org-firebase/storage.rules
new file mode 100644
index 000000000000..d494542e9b28
--- /dev/null
+++ b/scripts/code.angularjs.org-firebase/storage.rules
@@ -0,0 +1,7 @@
+service firebase.storage {
+ match /b/{bucket}/o {
+ match /{allPaths=**} {
+ allow read, write: if request.auth!=null;
+ }
+ }
+}
diff --git a/scripts/code.angularjs.org/publish.sh b/scripts/code.angularjs.org/publish.sh
index a9780a6de4a4..13420e725318 100755
--- a/scripts/code.angularjs.org/publish.sh
+++ b/scripts/code.angularjs.org/publish.sh
@@ -59,23 +59,12 @@ function _update_code() {
echo "-- Pushing code.angularjs.org"
git push origin master
-
- for backend in "$@" ; do
- echo "-- Refreshing code.angularjs.org: backend=$backend"
-
- # FIXME: We gave up publishing to code.angularjs.org because the GCE automatically removes firewall
- # rules that allow access to port 8003.
-
- # curl http://$backend:8003/gitFetchSite.php
- done
}
function publish {
- # The TXT record for backends.angularjs.org is a CSV of the IP addresses for
- # the currently serving Compute Engine backends.
- # code.angularjs.org is served out of port 8003 on these backends.
- backends=("$(dig backends.angularjs.org +short TXT | python -c 'print raw_input()[1:-1].replace(",", "\n")')")
- _update_code ${backends[@]}
+ # publish updates the code.angularjs.org Github repository
+ # the deployment to Firebase happens via Travis
+ _update_code
}
source $(dirname $0)/../utils.inc
diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh
index bf9be1724550..bc71d1d836be 100755
--- a/scripts/travis/build.sh
+++ b/scripts/travis/build.sh
@@ -5,39 +5,51 @@ set -e
export BROWSER_STACK_ACCESS_KEY=`echo $BROWSER_STACK_ACCESS_KEY | rev`
export SAUCE_ACCESS_KEY=`echo $SAUCE_ACCESS_KEY | rev`
-if [ "$JOB" == "ci-checks" ]; then
- grunt ci-checks
- if [[ $TRAVIS_PULL_REQUEST != 'false' ]]; then
- # validate commit messages of all commits in the PR
- # 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/.../..}"
- fi
-elif [ "$JOB" == "unit" ]; then
- if [ "$BROWSER_PROVIDER" == "browserstack" ]; then
- BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9"
- else
- BROWSERS="SL_Chrome,SL_Firefox,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_iOS"
- fi
-
- grunt test:promises-aplus
- grunt test:unit --browsers="$BROWSERS" --reporters=spec
- grunt tests:docs --browsers="$BROWSERS" --reporters=spec
-elif [ "$JOB" == "docs-e2e" ]; then
- grunt test:travis-protractor --specs="docs/app/e2e/**/*.scenario.js"
-elif [ "$JOB" == "e2e" ]; then
- if [[ $TEST_TARGET == jquery* ]]; then
- export USE_JQUERY=1
- fi
-
- export TARGET_SPECS="build/docs/ptore2e/**/default_test.js"
- if [[ "$TEST_TARGET" == jquery* ]]; then
- TARGET_SPECS="build/docs/ptore2e/**/jquery_test.js"
- fi
-
- export TARGET_SPECS="test/e2e/tests/**/*.js,$TARGET_SPECS"
- grunt test:travis-protractor --specs="$TARGET_SPECS"
-else
- echo "Unknown job type. Please set JOB=ci-checks, JOB=unit or JOB=e2e-*."
-fi
+case "$JOB" in
+ "ci-checks")
+ grunt ci-checks
+
+ if [[ $TRAVIS_PULL_REQUEST != 'false' ]]; then
+ # validate commit messages of all commits in the PR
+ # 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/.../..}"
+ fi
+ ;;
+ "unit")
+ if [ "$BROWSER_PROVIDER" == "browserstack" ]; then
+ BROWSERS="BS_Chrome,BS_Safari,BS_Firefox,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9"
+ else
+ BROWSERS="SL_Chrome,SL_Firefox,SL_Safari_8,SL_Safari_9,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_iOS"
+ fi
+
+ grunt test:promises-aplus
+ grunt test:unit --browsers="$BROWSERS" --reporters=spec
+ grunt tests:docs --browsers="$BROWSERS" --reporters=spec
+ ;;
+ "docs-e2e")
+ grunt test:travis-protractor --specs="docs/app/e2e/**/*.scenario.js"
+ ;;
+ "e2e")
+ if [[ $TEST_TARGET == jquery* ]]; then
+ export USE_JQUERY=1
+ fi
+
+ export TARGET_SPECS="build/docs/ptore2e/**/default_test.js"
+
+ if [[ "$TEST_TARGET" == jquery* ]]; then
+ TARGET_SPECS="build/docs/ptore2e/**/jquery_test.js"
+ fi
+
+ export TARGET_SPECS="test/e2e/tests/**/*.js,$TARGET_SPECS"
+ grunt test:travis-protractor --specs="$TARGET_SPECS"
+ ;;
+ "deploy")
+ grunt package
+ grunt compress:firebaseCodeDeploy
+ ;;
+ *)
+ echo "Unknown job type. Please set JOB=ci-checks, JOB=unit, JOB=deploy or JOB=e2e-*."
+ ;;
+esac
From 529550d0da3d88514ce9efb038fb935fbf90f971 Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Tue, 11 Jul 2017 00:35:30 -0700
Subject: [PATCH 015/552] refactor($parse): do not pass scope,locals to
interceptor fns
All internal use of interceptors are for things such as data
conversion/normalizing, never reading state from the scope/locals.
This is the intended use and makes interceptors more like filters
(which receive only the input value + args, no scope/locals).
---
src/ng/parse.js | 4 +--
test/ng/parseSpec.js | 68 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 70 insertions(+), 2 deletions(-)
diff --git a/src/ng/parse.js b/src/ng/parse.js
index 38bb20e7a2a2..731883334998 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -1940,12 +1940,12 @@ function $ParseProvider() {
function regularInterceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
- return interceptorFn(value, scope, locals);
+ return interceptorFn(value);
}
function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
- var result = interceptorFn(value, scope, locals);
+ var result = interceptorFn(value);
// we only return the interceptor's result if the
// initial value is defined (for bind-once)
return isDone(value) ? result : value;
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index cfa713d60279..e8edd15349b1 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -3232,6 +3232,74 @@ describe('parser', function() {
});
describe('interceptorFns', function() {
+ it('should only be passed the intercepted value', inject(function($parse) {
+ var args;
+ function interceptor(v) {
+ args = sliceArgs(arguments);
+ return v;
+ }
+
+ scope.$watch($parse('a', interceptor));
+
+ scope.a = 1;
+ scope.$digest();
+ expect(args).toEqual([1]);
+ }));
+
+ it('should only be passed the intercepted value when double-intercepted',
+ inject(function($parse) {
+ var args1;
+ function int1(v) {
+ args1 = sliceArgs(arguments);
+ return v + 2;
+ }
+ var args2;
+ function int2(v) {
+ args2 = sliceArgs(arguments);
+ return v + 4;
+ }
+
+ scope.$watch($parse($parse('a', int1), int2));
+
+ scope.a = 1;
+ scope.$digest();
+ expect(args1).toEqual([1]);
+ expect(args2).toEqual([3]);
+ }));
+
+ it('should support locals', inject(function($parse) {
+ var args;
+ function interceptor(v) {
+ args = sliceArgs(arguments);
+ return v + 4;
+ }
+
+ var exp = $parse('a + b', interceptor);
+ scope.a = 1;
+
+ expect(exp(scope, {b: 2})).toBe(7);
+ expect(args).toEqual([3]);
+ }));
+
+ it('should support locals when double-intercepted', inject(function($parse) {
+ var args1;
+ function int1(v) {
+ args1 = sliceArgs(arguments);
+ return v + 4;
+ }
+ var args2;
+ function int2(v) {
+ args2 = sliceArgs(arguments);
+ return v + 8;
+ }
+
+ var exp = $parse($parse('a + b', int1), int2);
+
+ scope.a = 1;
+ expect(exp(scope, {b: 2})).toBe(15);
+ expect(args1).toEqual([3]);
+ expect(args2).toEqual([7]);
+ }));
it('should always be invoked if they are flagged as having $stateful',
inject(function($parse) {
From de74034ddf6f92505ccdb61be413a6df2c723f87 Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Thu, 25 May 2017 23:43:34 -0700
Subject: [PATCH 016/552] fix($parse): respect the interceptor.$stateful flag
---
src/ng/parse.js | 44 ++++++++++++----------
test/ng/parseSpec.js | 89 ++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 113 insertions(+), 20 deletions(-)
diff --git a/src/ng/parse.js b/src/ng/parse.js
index 731883334998..6dd750afb8b3 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -1798,15 +1798,9 @@ function $ParseProvider() {
var lexer = new Lexer($parseOptions);
var parser = new Parser(lexer, $filter, $parseOptions);
parsedExpression = parser.parse(exp);
- if (parsedExpression.constant) {
- parsedExpression.$$watchDelegate = constantWatchDelegate;
- } else if (oneTime) {
- parsedExpression.oneTime = true;
- parsedExpression.$$watchDelegate = oneTimeWatchDelegate;
- } else if (parsedExpression.inputs) {
- parsedExpression.$$watchDelegate = inputsWatchDelegate;
- }
- cache[cacheKey] = parsedExpression;
+ parsedExpression.oneTime = !!oneTime;
+
+ cache[cacheKey] = addWatchDelegate(parsedExpression);
}
return addInterceptor(parsedExpression, interceptorFn);
@@ -1931,9 +1925,21 @@ function $ParseProvider() {
return unwatch;
}
+ function addWatchDelegate(parsedExpression) {
+ if (parsedExpression.constant) {
+ parsedExpression.$$watchDelegate = constantWatchDelegate;
+ } else if (parsedExpression.oneTime) {
+ parsedExpression.$$watchDelegate = oneTimeWatchDelegate;
+ } else if (parsedExpression.inputs) {
+ parsedExpression.$$watchDelegate = inputsWatchDelegate;
+ }
+
+ return parsedExpression;
+ }
+
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
- var watchDelegate = parsedExpression.$$watchDelegate;
+
var useInputs = false;
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
@@ -1953,18 +1959,16 @@ function $ParseProvider() {
var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression;
- // Propogate the literal/oneTime attributes
+ // Propogate the literal/oneTime/constant attributes
fn.literal = parsedExpression.literal;
fn.oneTime = parsedExpression.oneTime;
+ fn.constant = parsedExpression.constant;
- // Propagate or create inputs / $$watchDelegates
- useInputs = !parsedExpression.inputs;
- if (watchDelegate && watchDelegate !== inputsWatchDelegate) {
- fn.$$watchDelegate = watchDelegate;
- fn.inputs = parsedExpression.inputs;
- } else if (!interceptorFn.$stateful) {
- // Treat interceptor like filters - assume non-stateful by default and use the inputsWatchDelegate
- fn.$$watchDelegate = inputsWatchDelegate;
+ // Treat the interceptor like filters.
+ // If it is not $stateful then only watch its inputs.
+ // If the expression itself has no inputs then use the full expression as an input.
+ if (!interceptorFn.$stateful) {
+ useInputs = !parsedExpression.inputs;
fn.inputs = (parsedExpression.inputs ? parsedExpression.inputs : [parsedExpression]).map(function(e) {
// Remove the isPure flag of inputs when it is not absolute because they are now wrapped in a
// potentially non-pure interceptor function.
@@ -1975,7 +1979,7 @@ function $ParseProvider() {
});
}
- return fn;
+ return addWatchDelegate(fn);
}
}];
}
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index e8edd15349b1..07e3d3430869 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -3324,6 +3324,72 @@ describe('parser', function() {
expect(called).toBe(true);
}));
+ it('should always be invoked if flagged as $stateful when wrapping one-time',
+ inject(function($parse) {
+
+ var interceptorCalls = 0;
+ function interceptor() {
+ interceptorCalls++;
+ return 123;
+ }
+ interceptor.$stateful = true;
+
+ scope.$watch($parse('::a', interceptor));
+
+ interceptorCalls = 0;
+ scope.$digest();
+ expect(interceptorCalls).not.toBe(0);
+
+ interceptorCalls = 0;
+ scope.$digest();
+ expect(interceptorCalls).not.toBe(0);
+ }));
+
+ it('should always be invoked if flagged as $stateful when wrapping one-time with inputs',
+ inject(function($parse) {
+
+ $filterProvider.register('identity', valueFn(identity));
+
+ var interceptorCalls = 0;
+ function interceptor() {
+ interceptorCalls++;
+ return 123;
+ }
+ interceptor.$stateful = true;
+
+ scope.$watch($parse('::a | identity', interceptor));
+
+ interceptorCalls = 0;
+ scope.$digest();
+ expect(interceptorCalls).not.toBe(0);
+
+ interceptorCalls = 0;
+ scope.$digest();
+ expect(interceptorCalls).not.toBe(0);
+ }));
+
+ it('should always be invoked if flagged as $stateful when wrapping one-time literal',
+ inject(function($parse) {
+
+ var interceptorCalls = 0;
+ function interceptor() {
+ interceptorCalls++;
+ return 123;
+ }
+ interceptor.$stateful = true;
+
+ scope.$watch($parse('::[a]', interceptor));
+
+ // Would be great if interceptor-output was checked for changes and this didn't throw...
+ interceptorCalls = 0;
+ expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
+ expect(interceptorCalls).not.toBe(0);
+
+ interceptorCalls = 0;
+ expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
+ expect(interceptorCalls).not.toBe(0);
+ }));
+
it('should not be invoked unless the input changes', inject(function($parse) {
var called = false;
function interceptor(v) {
@@ -3434,6 +3500,29 @@ describe('parser', function() {
scope.$digest();
expect(scope.$$watchersCount).toBe(0);
}));
+
+ it('should not propogate $$watchDelegate to the interceptor wrapped expression', inject(function($parse) {
+ function getter(s) {
+ return s.x;
+ }
+ getter.$$watchDelegate = getter;
+
+ function doubler(v) {
+ return 2 * v;
+ }
+
+ var lastValue;
+ function watcher(val) {
+ lastValue = val;
+ }
+ scope.$watch($parse(getter, doubler), watcher);
+
+ scope.$apply('x = 1');
+ expect(lastValue).toBe(2 * 1);
+
+ scope.$apply('x = 123');
+ expect(lastValue).toBe(2 * 123);
+ }));
});
describe('literals', function() {
From 2ee5033967d5f87a516bad137686b0592e25d26b Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Fri, 16 Jun 2017 01:32:34 -0700
Subject: [PATCH 017/552] fix($parse): always pass the intercepted value to
watchers
Fixes #16021
---
src/ng/directive/ngClass.js | 6 ---
src/ng/parse.js | 79 ++++++++++++++++++++++---------------
test/ng/interpolateSpec.js | 16 ++++++++
test/ng/parseSpec.js | 56 ++++++++++++++++++++++++--
4 files changed, 117 insertions(+), 40 deletions(-)
diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js
index 090c2c387f8c..e289a4c10441 100644
--- a/src/ng/directive/ngClass.js
+++ b/src/ng/directive/ngClass.js
@@ -91,12 +91,6 @@ function classDirective(name, selector) {
}
function ngClassWatchAction(newClassString) {
- // When using a one-time binding the newClassString will return
- // the pre-interceptor value until the one-time is complete
- if (!isString(newClassString)) {
- newClassString = toClassString(newClassString);
- }
-
if (oldModulo === selector) {
updateClasses(oldClassString, newClassString);
}
diff --git a/src/ng/parse.js b/src/ng/parse.js
index 6dd750afb8b3..f1b3de867eb0 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -1884,28 +1884,37 @@ function $ParseProvider() {
function oneTimeWatchDelegate(scope, listener, objectEquality, parsedExpression, prettyPrintExpression) {
var isDone = parsedExpression.literal ? isAllDefined : isDefined;
var unwatch, lastValue;
- if (parsedExpression.inputs) {
- unwatch = inputsWatchDelegate(scope, oneTimeListener, objectEquality, parsedExpression, prettyPrintExpression);
- } else {
- unwatch = scope.$watch(oneTimeWatch, oneTimeListener, objectEquality);
- }
+
+ var exp = parsedExpression.$$intercepted || parsedExpression;
+ var post = parsedExpression.$$interceptor || identity;
+
+ var useInputs = parsedExpression.inputs && !exp.inputs;
+
+ // Propogate the literal/inputs/constant attributes
+ // ... but not oneTime since we are handling it
+ oneTimeWatch.literal = parsedExpression.literal;
+ oneTimeWatch.constant = parsedExpression.constant;
+ oneTimeWatch.inputs = parsedExpression.inputs;
+
+ // Allow other delegates to run on this wrapped expression
+ addWatchDelegate(oneTimeWatch);
+
+ unwatch = scope.$watch(oneTimeWatch, listener, objectEquality, prettyPrintExpression);
+
return unwatch;
- function oneTimeWatch(scope) {
- return parsedExpression(scope);
- }
- function oneTimeListener(value, old, scope) {
- lastValue = value;
- if (isFunction(listener)) {
- listener(value, old, scope);
+ function unwatchIfDone() {
+ if (isDone(lastValue)) {
+ unwatch();
}
- if (isDone(value)) {
- scope.$$postDigest(function() {
- if (isDone(lastValue)) {
- unwatch();
- }
- });
+ }
+
+ function oneTimeWatch(scope, locals, assign, inputs) {
+ lastValue = useInputs && inputs ? inputs[0] : exp(scope, locals, assign, inputs);
+ if (isDone(lastValue)) {
+ scope.$$postDigest(unwatchIfDone);
}
+ return post(lastValue, scope, locals);
}
}
@@ -1937,27 +1946,35 @@ function $ParseProvider() {
return parsedExpression;
}
+ function chainInterceptors(first, second) {
+ function chainedInterceptor(value) {
+ return second(first(value));
+ }
+ chainedInterceptor.$stateful = first.$stateful || second.$stateful;
+
+ return chainedInterceptor;
+ }
+
function addInterceptor(parsedExpression, interceptorFn) {
if (!interceptorFn) return parsedExpression;
- var useInputs = false;
+ // Extract any existing interceptors out of the parsedExpression
+ // to ensure the original parsedExpression is always the $$intercepted
+ if (parsedExpression.$$interceptor) {
+ interceptorFn = chainInterceptors(parsedExpression.$$interceptor, interceptorFn);
+ parsedExpression = parsedExpression.$$intercepted;
+ }
- var isDone = parsedExpression.literal ? isAllDefined : isDefined;
+ var useInputs = false;
- function regularInterceptedExpression(scope, locals, assign, inputs) {
+ var fn = function interceptedExpression(scope, locals, assign, inputs) {
var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
return interceptorFn(value);
- }
-
- function oneTimeInterceptedExpression(scope, locals, assign, inputs) {
- var value = useInputs && inputs ? inputs[0] : parsedExpression(scope, locals, assign, inputs);
- var result = interceptorFn(value);
- // we only return the interceptor's result if the
- // initial value is defined (for bind-once)
- return isDone(value) ? result : value;
- }
+ };
- var fn = parsedExpression.oneTime ? oneTimeInterceptedExpression : regularInterceptedExpression;
+ // Maintain references to the interceptor/intercepted
+ fn.$$intercepted = parsedExpression;
+ fn.$$interceptor = interceptorFn;
// Propogate the literal/oneTime/constant attributes
fn.literal = parsedExpression.literal;
diff --git a/test/ng/interpolateSpec.js b/test/ng/interpolateSpec.js
index f8ed846d93d2..2ed9b31b7f5f 100644
--- a/test/ng/interpolateSpec.js
+++ b/test/ng/interpolateSpec.js
@@ -149,6 +149,22 @@ describe('$interpolate', function() {
expect($rootScope.$countWatchers()).toBe(0);
}));
+ it('should respect one-time bindings for literals', inject(function($interpolate, $rootScope) {
+ var calls = [];
+ $rootScope.$watch($interpolate('{{ ::{x: x} }}'), function(val) {
+ calls.push(val);
+ });
+
+ $rootScope.$apply();
+ expect(calls.pop()).toBe('{}');
+
+ $rootScope.$apply('x = 1');
+ expect(calls.pop()).toBe('{"x":1}');
+
+ $rootScope.$apply('x = 2');
+ expect(calls.pop()).toBeUndefined();
+ }));
+
it('should stop watching strings with no expressions after first execution',
inject(function($interpolate, $rootScope) {
var spy = jasmine.createSpy();
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index 07e3d3430869..025cf8ec50b0 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -3380,13 +3380,12 @@ describe('parser', function() {
scope.$watch($parse('::[a]', interceptor));
- // Would be great if interceptor-output was checked for changes and this didn't throw...
interceptorCalls = 0;
- expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
+ scope.$digest();
expect(interceptorCalls).not.toBe(0);
interceptorCalls = 0;
- expect(function() { scope.$digest(); }).toThrowMinErr('$rootScope', 'infdig');
+ scope.$digest();
expect(interceptorCalls).not.toBe(0);
}));
@@ -3501,6 +3500,57 @@ describe('parser', function() {
expect(scope.$$watchersCount).toBe(0);
}));
+ it('should watch the intercepted value of one-time bindings', inject(function($parse, log) {
+ scope.$watch($parse('::{x:x, y:y}', function(lit) { return lit.x; }), log);
+
+ scope.$apply();
+ expect(log.empty()).toEqual([undefined]);
+
+ scope.$apply('x = 1');
+ expect(log.empty()).toEqual([1]);
+
+ scope.$apply('x = 2; y=1');
+ expect(log.empty()).toEqual([2]);
+
+ scope.$apply('x = 1; y=2');
+ expect(log.empty()).toEqual([]);
+ }));
+
+ it('should watch the intercepted value of one-time bindings in nested interceptors', inject(function($parse, log) {
+ scope.$watch($parse($parse('::{x:x, y:y}', function(lit) { return lit.x; }), identity), log);
+
+ scope.$apply();
+ expect(log.empty()).toEqual([undefined]);
+
+ scope.$apply('x = 1');
+ expect(log.empty()).toEqual([1]);
+
+ scope.$apply('x = 2; y=1');
+ expect(log.empty()).toEqual([2]);
+
+ scope.$apply('x = 1; y=2');
+ expect(log.empty()).toEqual([]);
+ }));
+
+ it('should nest interceptors around eachother, not around the intercepted', inject(function($parse) {
+ function origin() { return 0; }
+
+ var fn = origin;
+ function addOne(n) { return n + 1; }
+
+ fn = $parse(fn, addOne);
+ expect(fn.$$intercepted).toBe(origin);
+ expect(fn()).toBe(1);
+
+ fn = $parse(fn, addOne);
+ expect(fn.$$intercepted).toBe(origin);
+ expect(fn()).toBe(2);
+
+ fn = $parse(fn, addOne);
+ expect(fn.$$intercepted).toBe(origin);
+ expect(fn()).toBe(3);
+ }));
+
it('should not propogate $$watchDelegate to the interceptor wrapped expression', inject(function($parse) {
function getter(s) {
return s.x;
From 2fb2d09971c203cc0164b3100cd9b43b484aeece Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Fri, 16 Jun 2017 08:53:33 -0700
Subject: [PATCH 018/552] test(ngClass): add test for one-time objects in array
literal
---
test/ng/directive/ngClassSpec.js | 14 ++++++++++++++
1 file changed, 14 insertions(+)
diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js
index 1c08a1c4133c..0180d67a6aa4 100644
--- a/test/ng/directive/ngClassSpec.js
+++ b/test/ng/directive/ngClassSpec.js
@@ -532,6 +532,20 @@ describe('ngClass', function() {
})
);
+ it('should support a one-time mixed literal-array/object variable', inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+
+ $rootScope.classVar1 = {orange: true};
+ $rootScope.$digest();
+ expect(element).toHaveClass('orange');
+
+ $rootScope.classVar1.orange = false;
+ $rootScope.$digest();
+
+ expect(element).not.toHaveClass('orange');
+ })
+ );
+
it('should do value stabilization as expected when one-time binding',
inject(function($rootScope, $compile) {
From 8de97949c58a95c348a1b95198c0747eb908d18a Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Sun, 16 Jul 2017 22:13:44 -0700
Subject: [PATCH 019/552] fix($parse): support constants in computed keys
---
src/ng/parse.js | 3 ++-
test/ng/parseSpec.js | 5 +++++
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/src/ng/parse.js b/src/ng/parse.js
index f1b3de867eb0..40aeebe00330 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -745,12 +745,13 @@ function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
argsToWatch = [];
forEach(ast.properties, function(property) {
findConstantAndWatchExpressions(property.value, $filter, astIsPure);
- allConstants = allConstants && property.value.constant && !property.computed;
+ allConstants = allConstants && property.value.constant;
if (!property.value.constant) {
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
}
if (property.computed) {
findConstantAndWatchExpressions(property.key, $filter, astIsPure);
+ allConstants = allConstants && property.key.constant;
if (!property.key.constant) {
argsToWatch.push.apply(argsToWatch, property.key.toWatch);
}
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index 025cf8ec50b0..9db0cd701757 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -4009,6 +4009,11 @@ describe('parser', function() {
expect($parse('5 != null').constant).toBe(true);
expect($parse('{standard: 4/3, wide: 16/9}').constant).toBe(true);
expect($parse('{[standard]: 4/3, wide: 16/9}').constant).toBe(false);
+ expect($parse('{["key"]: 1}').constant).toBe(true);
+ expect($parse('[0].length').constant).toBe(true);
+ expect($parse('[0][0]').constant).toBe(true);
+ expect($parse('{x: 1}.x').constant).toBe(true);
+ expect($parse('{x: 1}["x"]').constant).toBe(true);
}));
it('should not mark any expression involving variables or function calls as constant', inject(function($parse) {
From 341f8dbe2484c9841c4fbf405ba5438029175675 Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Sun, 16 Jul 2017 22:12:25 -0700
Subject: [PATCH 020/552] refactor($parse): remove unnecessary .constant if
when collecting inputs
---
src/ng/parse.js | 19 +++++--------------
1 file changed, 5 insertions(+), 14 deletions(-)
diff --git a/src/ng/parse.js b/src/ng/parse.js
index 40aeebe00330..0f314181b847 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -705,7 +705,7 @@ function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
findConstantAndWatchExpressions(ast.property, $filter, astIsPure);
}
ast.constant = ast.object.constant && (!ast.computed || ast.property.constant);
- ast.toWatch = [ast];
+ ast.toWatch = ast.constant ? [] : [ast];
break;
case AST.CallExpression:
isStatelessFilter = ast.filter ? isStateless($filter, ast.callee.name) : false;
@@ -714,9 +714,7 @@ function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
forEach(ast.arguments, function(expr) {
findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
- if (!expr.constant) {
- argsToWatch.push.apply(argsToWatch, expr.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
ast.toWatch = isStatelessFilter ? argsToWatch : [ast];
@@ -733,9 +731,7 @@ function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
forEach(ast.elements, function(expr) {
findConstantAndWatchExpressions(expr, $filter, astIsPure);
allConstants = allConstants && expr.constant;
- if (!expr.constant) {
- argsToWatch.push.apply(argsToWatch, expr.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, expr.toWatch);
});
ast.constant = allConstants;
ast.toWatch = argsToWatch;
@@ -746,17 +742,12 @@ function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
forEach(ast.properties, function(property) {
findConstantAndWatchExpressions(property.value, $filter, astIsPure);
allConstants = allConstants && property.value.constant;
- if (!property.value.constant) {
- argsToWatch.push.apply(argsToWatch, property.value.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, property.value.toWatch);
if (property.computed) {
findConstantAndWatchExpressions(property.key, $filter, astIsPure);
allConstants = allConstants && property.key.constant;
- if (!property.key.constant) {
- argsToWatch.push.apply(argsToWatch, property.key.toWatch);
- }
+ argsToWatch.push.apply(argsToWatch, property.key.toWatch);
}
-
});
ast.constant = allConstants;
ast.toWatch = argsToWatch;
From 631076a31bf0bb7ab78c0504a81eed006805c182 Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Sun, 16 Jul 2017 22:15:49 -0700
Subject: [PATCH 021/552] fix($parse): do not shallow-watch computed property
keys
Shallow watching is not enough when an object implements a non-pure toString
---
src/ng/parse.js | 3 ++-
test/ng/parseSpec.js | 36 ++++++++++++++++++++++++++++--------
2 files changed, 30 insertions(+), 9 deletions(-)
diff --git a/src/ng/parse.js b/src/ng/parse.js
index 0f314181b847..b2ef9f27d4e4 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -744,7 +744,8 @@ function findConstantAndWatchExpressions(ast, $filter, parentIsPure) {
allConstants = allConstants && property.value.constant;
argsToWatch.push.apply(argsToWatch, property.value.toWatch);
if (property.computed) {
- findConstantAndWatchExpressions(property.key, $filter, astIsPure);
+ //`{[key]: value}` implicitly does `key.toString()` which may be non-pure
+ findConstantAndWatchExpressions(property.key, $filter, /*parentIsPure=*/false);
allConstants = allConstants && property.key.constant;
argsToWatch.push.apply(argsToWatch, property.key.toWatch);
}
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index 9db0cd701757..8e6b6a943634 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -3805,35 +3805,55 @@ describe('parser', function() {
it('should watch ES6 object computed property changes', function() {
var count = 0;
- var values = [];
+ var lastValue;
scope.$watch('{[a]: true}', function(val) {
count++;
- values.push(val);
- }, true);
+ lastValue = val;
+ });
scope.$digest();
expect(count).toBe(1);
- expect(values[0]).toEqual({'undefined': true});
+ expect(lastValue).toEqual({'undefined': true});
scope.$digest();
expect(count).toBe(1);
- expect(values[0]).toEqual({'undefined': true});
+ expect(lastValue).toEqual({'undefined': true});
scope.a = true;
scope.$digest();
expect(count).toBe(2);
- expect(values[1]).toEqual({'true': true});
+ expect(lastValue).toEqual({'true': true});
scope.a = 'abc';
scope.$digest();
expect(count).toBe(3);
- expect(values[2]).toEqual({'abc': true});
+ expect(lastValue).toEqual({'abc': true});
scope.a = undefined;
scope.$digest();
expect(count).toBe(4);
- expect(values[3]).toEqual({'undefined': true});
+ expect(lastValue).toEqual({'undefined': true});
+ });
+
+ it('should not shallow-watch ES6 object computed properties in case of stateful toString', function() {
+ var count = 0;
+ var lastValue;
+
+ scope.$watch('{[a]: true}', function(val) {
+ count++;
+ lastValue = val;
+ });
+
+ scope.a = {toString: function() { return this.b; }};
+ scope.a.b = 1;
+
+ //TODO: would be great if it didn't throw!
+ expect(function() { scope.$apply(); }).toThrowMinErr('$rootScope', 'infdig');
+ expect(lastValue).toEqual({1: true});
+
+ expect(function() { scope.$apply('a.b = 2'); }).toThrowMinErr('$rootScope', 'infdig');
+ expect(lastValue).toEqual({2: true});
});
});
From 823c7edb84e2698f035a6712e32dfb4e95be888c Mon Sep 17 00:00:00 2001
From: Kerry McCullough
Date: Thu, 13 Jul 2017 11:41:36 -0600
Subject: [PATCH 022/552] feat($resource): add resource to response for error
interceptors
Closes #16109
---
src/ngResource/resource.js | 13 ++++++++++---
test/ngResource/resourceSpec.js | 1 +
2 files changed, 11 insertions(+), 3 deletions(-)
diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js
index 054fb2523ac6..4fb1ff9f28f6 100644
--- a/src/ngResource/resource.js
+++ b/src/ngResource/resource.js
@@ -192,7 +192,12 @@ function shallowClearAndCopy(src, dst) {
* [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
* - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
* `response` and `responseError`. Both `response` and `responseError` interceptors get called
- * with `http response` object. See {@link ng.$http $http interceptors}.
+ * with `http response` object. See {@link ng.$http $http interceptors}. In addition, the
+ * resource instance or array object is accessible by the `resource` property of the
+ * `http response` object.
+ * Keep in mind that the associated promise will be resolved with the value returned by the
+ * response interceptor, if one is specified. The default response interceptor returns
+ * `response.resource` (i.e. the resource instance or array).
* - **`hasBody`** - `{boolean}` - allows to specify if a request body should be included or not.
* If not specified only POST, PUT and PATCH requests will have a body.
*
@@ -267,8 +272,7 @@ function shallowClearAndCopy(src, dst) {
* {@link ngRoute.$routeProvider resolve section of $routeProvider.when()} to defer view
* rendering until the resource(s) are loaded.
*
- * On failure, the promise is rejected with the {@link ng.$http http response} object, without
- * the `resource` property.
+ * On failure, the promise is rejected with the {@link ng.$http http response} object.
*
* If an interceptor object was provided, the promise will instead be resolved with the value
* returned by the interceptor.
@@ -776,6 +780,9 @@ angular.module('ngResource', ['ng']).
response.resource = value;
return response;
+ }, function(response) {
+ response.resource = value;
+ return $q.reject(response);
});
promise = promise['finally'](function() {
diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js
index bc8bbd079642..869ffa61aabc 100644
--- a/test/ngResource/resourceSpec.js
+++ b/test/ngResource/resourceSpec.js
@@ -1205,6 +1205,7 @@ describe('basic usage', function() {
expect(callback).toHaveBeenCalledOnce();
var response = callback.calls.mostRecent().args[0];
+ expect(response.resource).toBe(ccs);
expect(response.status).toBe(404);
expect(response.config).toBeDefined();
});
From e6d5fe7dea795d02eb2eab6ac69aca81a3edc99d Mon Sep 17 00:00:00 2001
From: sathify
Date: Fri, 29 Aug 2014 14:42:22 -0700
Subject: [PATCH 023/552] style(css) separate selectors and declarations by new
lines
---
css/angular-scenario.css | 3 ++-
css/angular.css | 8 ++++++--
2 files changed, 8 insertions(+), 3 deletions(-)
diff --git a/css/angular-scenario.css b/css/angular-scenario.css
index b8d25c1c08d0..56032ed777fe 100644
--- a/css/angular-scenario.css
+++ b/css/angular-scenario.css
@@ -13,7 +13,8 @@ body {
text-align: center;
}
-#json, #xml {
+#json,
+#xml {
display: none;
}
diff --git a/css/angular.css b/css/angular.css
index a2921a61c9e1..8b3915383e3e 100644
--- a/css/angular.css
+++ b/css/angular.css
@@ -1,7 +1,11 @@
@charset "UTF-8";
-[ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],
-.ng-cloak, .x-ng-cloak,
+[ng\:cloak],
+[ng-cloak],
+[data-ng-cloak],
+[x-ng-cloak],
+.ng-cloak,
+.x-ng-cloak,
.ng-hide:not(.ng-hide-animate) {
display: none !important;
}
From 87a586eb9a23cfd0d0bb681cc778b4b8e5c8451d Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Wed, 19 Jul 2017 22:38:10 -0700
Subject: [PATCH 024/552] fix($parse): do not pass scope/locals to interceptors
of one-time bindings
---
src/ng/parse.js | 2 +-
test/ng/parseSpec.js | 14 ++++++++++++++
2 files changed, 15 insertions(+), 1 deletion(-)
diff --git a/src/ng/parse.js b/src/ng/parse.js
index b2ef9f27d4e4..f173eed527d8 100644
--- a/src/ng/parse.js
+++ b/src/ng/parse.js
@@ -1907,7 +1907,7 @@ function $ParseProvider() {
if (isDone(lastValue)) {
scope.$$postDigest(unwatchIfDone);
}
- return post(lastValue, scope, locals);
+ return post(lastValue);
}
}
diff --git a/test/ng/parseSpec.js b/test/ng/parseSpec.js
index 8e6b6a943634..7c5baad1630b 100644
--- a/test/ng/parseSpec.js
+++ b/test/ng/parseSpec.js
@@ -3246,6 +3246,20 @@ describe('parser', function() {
expect(args).toEqual([1]);
}));
+ it('should only be passed the intercepted value when wrapping one-time', inject(function($parse) {
+ var args;
+ function interceptor(v) {
+ args = sliceArgs(arguments);
+ return v;
+ }
+
+ scope.$watch($parse('::a', interceptor));
+
+ scope.a = 1;
+ scope.$digest();
+ expect(args).toEqual([1]);
+ }));
+
it('should only be passed the intercepted value when double-intercepted',
inject(function($parse) {
var args1;
From 15bbd3e18cd89b91f7206a06c73d40e54a8a48a0 Mon Sep 17 00:00:00 2001
From: gdi2290
Date: Fri, 19 Dec 2014 17:11:25 -0800
Subject: [PATCH 025/552] perf(ngStyleDirective): use $watchCollection
Since we are simply watching a flat object collection it is more performant
to use $watchCollection than a deepWatch...
BREAKING CHANGE:
Previously the use of deep watch by ng-style would trigger styles to be
re-applied when nested state changed. Now only changes to direct
properties of the watched object will trigger changes.
Closes #15947
---
src/ng/directive/ngStyle.js | 4 ++--
test/ng/directive/ngStyleSpec.js | 15 +++++++++++++++
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/src/ng/directive/ngStyle.js b/src/ng/directive/ngStyle.js
index 844029904202..afb7a79d269d 100644
--- a/src/ng/directive/ngStyle.js
+++ b/src/ng/directive/ngStyle.js
@@ -52,10 +52,10 @@
*/
var ngStyleDirective = ngDirective(function(scope, element, attr) {
- scope.$watch(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
+ scope.$watchCollection(attr.ngStyle, function ngStyleWatchAction(newStyles, oldStyles) {
if (oldStyles && (newStyles !== oldStyles)) {
forEach(oldStyles, function(val, style) { element.css(style, '');});
}
if (newStyles) element.css(newStyles);
- }, true);
+ });
});
diff --git a/test/ng/directive/ngStyleSpec.js b/test/ng/directive/ngStyleSpec.js
index b708eb7a4d82..38bf4399235f 100644
--- a/test/ng/directive/ngStyleSpec.js
+++ b/test/ng/directive/ngStyleSpec.js
@@ -23,6 +23,21 @@ describe('ngStyle', function() {
}));
+ it('should not deep watch objects', inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ expect(parseInt(element.css('height') + 0, 10)).toEqual(0); // height could be '' or '0px'
+ $rootScope.heightObj = {toString: function() { return '40px'; }};
+ $rootScope.$digest();
+ expect(element.css('height')).toBe('40px');
+
+ element.css('height', '10px');
+ $rootScope.heightObj.otherProp = 123;
+ $rootScope.$digest();
+ expect(element.css('height')).toBe('10px');
+ }));
+
+
it('should support lazy one-time binding for object literals', inject(function($rootScope, $compile) {
element = $compile('')($rootScope);
$rootScope.$digest();
From ac57a25cd981fe3ffcfdfeea1e1ac3fccf787696 Mon Sep 17 00:00:00 2001
From: Jason Bedard
Date: Thu, 4 May 2017 22:39:21 -0700
Subject: [PATCH 026/552] test(ngStyle): add test for object literal
---
test/ng/directive/ngStyleSpec.js | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/test/ng/directive/ngStyleSpec.js b/test/ng/directive/ngStyleSpec.js
index 38bf4399235f..7a8cfb2cd75f 100644
--- a/test/ng/directive/ngStyleSpec.js
+++ b/test/ng/directive/ngStyleSpec.js
@@ -38,6 +38,18 @@ describe('ngStyle', function() {
}));
+ it('should support binding for object literals', inject(function($rootScope, $compile) {
+ element = $compile('')($rootScope);
+ $rootScope.$digest();
+ expect(parseInt(element.css('height') + 0, 10)).toEqual(0); // height could be '' or '0px'
+ $rootScope.$apply('heightStr = "40px"');
+ expect(element.css('height')).toBe('40px');
+
+ $rootScope.$apply('heightStr = "100px"');
+ expect(element.css('height')).toBe('100px');
+ }));
+
+
it('should support lazy one-time binding for object literals', inject(function($rootScope, $compile) {
element = $compile('')($rootScope);
$rootScope.$digest();
From 01d6a47e91e60f99d093d90968adb32f8c24615a Mon Sep 17 00:00:00 2001
From: Chirag Bhatia
Date: Sun, 16 Jul 2017 00:37:48 +0530
Subject: [PATCH 027/552] fix($resource): do not throw when calling old
`$cancelRequest()`
Closes #16037
---
src/ngResource/resource.js | 4 +++-
test/ngResource/resourceSpec.js | 19 +++++++++++++++++++
2 files changed, 22 insertions(+), 1 deletion(-)
diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js
index 4fb1ff9f28f6..861b6bebb0f9 100644
--- a/src/ngResource/resource.js
+++ b/src/ngResource/resource.js
@@ -830,7 +830,9 @@ angular.module('ngResource', ['ng']).
function cancelRequest(value) {
promise.catch(noop);
- timeoutDeferred.resolve(value);
+ if (timeoutDeferred !== null) {
+ timeoutDeferred.resolve(value);
+ }
}
};
diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js
index 869ffa61aabc..1f0f61282b30 100644
--- a/test/ngResource/resourceSpec.js
+++ b/test/ngResource/resourceSpec.js
@@ -2102,6 +2102,25 @@ describe('cancelling requests', function() {
expect(creditCard.$cancelRequest).toBe(noop);
});
+
+ it('should not break when calling old `$cancelRequest` after the response arrives', function() {
+ $httpBackend.whenGET('/CreditCard').respond({});
+
+ var CreditCard = $resource('/CreditCard', {}, {
+ get: {
+ method: 'GET',
+ cancellable: true
+ }
+ });
+
+ var creditCard = CreditCard.get();
+ var cancelRequest = creditCard.$cancelRequest;
+
+ $httpBackend.flush();
+
+ expect(cancelRequest).not.toBe(noop);
+ expect(cancelRequest).not.toThrow();
+ });
});
describe('configuring `cancellable` on the provider', function() {
From f1d01bbc748e033035107dbb4259fe40d3443dfb Mon Sep 17 00:00:00 2001
From: Zita Nemeckova
Date: Tue, 25 Jul 2017 12:31:07 +0200
Subject: [PATCH 028/552] feat($compile): add
`strictComponentBindingsEnabled()` method
Closes #16129
---
docs/content/error/$compile/missingattr.ngdoc | 8 +
src/ng/compile.js | 41 ++++
test/ng/compileSpec.js | 223 ++++++++++++++++++
3 files changed, 272 insertions(+)
create mode 100644 docs/content/error/$compile/missingattr.ngdoc
diff --git a/docs/content/error/$compile/missingattr.ngdoc b/docs/content/error/$compile/missingattr.ngdoc
new file mode 100644
index 000000000000..1fb2a346b4a2
--- /dev/null
+++ b/docs/content/error/$compile/missingattr.ngdoc
@@ -0,0 +1,8 @@
+@ngdoc error
+@name $compile:missingattr
+@fullName Missing required attribute
+@description
+
+This error may occur only when `$compileProvider.strictComponentBindingsEnabled` is set to `true`.
+Then all attributes mentioned in `bindings` without `?` must be set. If one or more aren't set,
+the first one will throw an error.
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 44b641bd3f17..615e42516fc1 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -1403,6 +1403,32 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
return debugInfoEnabled;
};
+ /**
+ * @ngdoc method
+ * @name $compileProvider#strictComponentBindingsEnabled
+ *
+ * @param {boolean=} enabled update the strictComponentBindingsEnabled state if provided, otherwise just return the
+ * current strictComponentBindingsEnabled state
+ * @returns {*} current value if used as getter or itself (chaining) if used as setter
+ *
+ * @kind function
+ *
+ * @description
+ * Call this method to enable/disable strict component bindings check. If enabled, the compiler will enforce that
+ * for all bindings of a component that are not set as optional with `?`, an attribute needs to be provided
+ * on the component's HTML tag.
+ *
+ * The default value is false.
+ */
+ var strictComponentBindingsEnabled = false;
+ this.strictComponentBindingsEnabled = function(enabled) {
+ if (isDefined(enabled)) {
+ strictComponentBindingsEnabled = enabled;
+ return this;
+ }
+ return strictComponentBindingsEnabled;
+ };
+
var TTL = 10;
/**
* @ngdoc method
@@ -3413,12 +3439,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
}
+ function strictBindingsCheck(attrName, directiveName) {
+ if (strictComponentBindingsEnabled) {
+ throw $compileMinErr('missingattr',
+ 'Attribute \'{0}\' of \'{1}\' is non-optional and must be set!',
+ attrName, directiveName);
+ }
+ }
// Set up $watches for isolate scope and controller bindings.
function initializeDirectiveBindings(scope, attrs, destination, bindings, directive) {
var removeWatchCollection = [];
var initialChanges = {};
var changes;
+
forEach(bindings, function initializeBinding(definition, scopeName) {
var attrName = definition.attrName,
optional = definition.optional,
@@ -3430,7 +3464,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
case '@':
if (!optional && !hasOwnProperty.call(attrs, attrName)) {
+ strictBindingsCheck(attrName, directive.name);
destination[scopeName] = attrs[attrName] = undefined;
+
}
removeWatch = attrs.$observe(attrName, function(value) {
if (isString(value) || isBoolean(value)) {
@@ -3457,6 +3493,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
case '=':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
+ strictBindingsCheck(attrName, directive.name);
attrs[attrName] = undefined;
}
if (optional && !attrs[attrName]) break;
@@ -3501,6 +3538,7 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
case '<':
if (!hasOwnProperty.call(attrs, attrName)) {
if (optional) break;
+ strictBindingsCheck(attrName, directive.name);
attrs[attrName] = undefined;
}
if (optional && !attrs[attrName]) break;
@@ -3526,6 +3564,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
break;
case '&':
+ if (!optional && !hasOwnProperty.call(attrs, attrName)) {
+ strictBindingsCheck(attrName, directive.name);
+ }
// Don't assign Object.prototype method to scope
parentGet = attrs.hasOwnProperty(attrName) ? $parse(attrs[attrName]) : noop;
diff --git a/test/ng/compileSpec.js b/test/ng/compileSpec.js
index 4fa14d2daff0..e6036555627c 100644
--- a/test/ng/compileSpec.js
+++ b/test/ng/compileSpec.js
@@ -169,6 +169,15 @@ describe('$compile', function() {
inject();
});
+ it('should allow strictComponentBindingsEnabled to be configured', function() {
+ module(function($compileProvider) {
+ expect($compileProvider.strictComponentBindingsEnabled()).toBe(false); // the default
+ $compileProvider.strictComponentBindingsEnabled(true);
+ expect($compileProvider.strictComponentBindingsEnabled()).toBe(true);
+ });
+ inject();
+ });
+
it('should allow onChangesTtl to be configured', function() {
module(function($compileProvider) {
expect($compileProvider.onChangesTtl()).toBe(10); // the default
@@ -2546,6 +2555,16 @@ describe('$compile', function() {
template: ''
};
});
+ directive('prototypeMethodNameAsScopeVarD', function() {
+ return {
+ scope: {
+ 'constructor': '',
+ 'valueOf': '<'
+ },
+ restrict: 'AE',
+ template: ''
+ };
+ });
directive('watchAsScopeVar', function() {
return {
scope: {
@@ -2854,6 +2873,57 @@ describe('$compile', function() {
})
);
+ it('should throw an error for undefined non-optional "=" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).toThrowMinErr('$compile',
+ 'missingattr',
+ 'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
+ 'ScopeVarA\' is non-optional and must be set!');
+ });
+ });
+
+ it('should not throw an error for set non-optional "=" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
+ it('should not throw an error for undefined optional "=" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
it('should handle "@" bindings with same method names in Object.prototype correctly when not present', inject(
function($rootScope, $compile) {
var func = function() {
@@ -2891,6 +2961,57 @@ describe('$compile', function() {
})
);
+ it('should throw an error for undefined non-optional "@" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).toThrowMinErr('$compile',
+ 'missingattr',
+ 'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
+ 'ScopeVarB\' is non-optional and must be set!');
+ });
+ });
+
+ it('should not throw an error for set non-optional "@" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
+ it('should not throw an error for undefined optional "@" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
it('should handle "&" bindings with same method names in Object.prototype correctly when not present', inject(
function($rootScope, $compile) {
var func = function() {
@@ -2923,6 +3044,108 @@ describe('$compile', function() {
})
);
+ it('should throw an error for undefined non-optional "&" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).toThrowMinErr('$compile',
+ 'missingattr',
+ 'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
+ 'ScopeVarC\' is non-optional and must be set!');
+ });
+ });
+
+ it('should not throw an error for set non-optional "&" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
+ it('should not throw an error for undefined optional "&" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
+ it('should throw an error for undefined non-optional "<" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).toThrowMinErr('$compile',
+ 'missingattr',
+ 'Attribute \'valueOf\' of \'prototypeMethodNameAs' +
+ 'ScopeVarD\' is non-optional and must be set!');
+ });
+ });
+
+ it('should not throw an error for set non-optional "<" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
+ it('should not throw an error for undefined optional "<" bindings when ' +
+ 'strictComponentBindingsEnabled is true', function() {
+ module(function($compileProvider) {
+ $compileProvider.strictComponentBindingsEnabled(true);
+ });
+ inject(
+ function($rootScope, $compile) {
+ var func = function() {
+ element = $compile(
+ ''
+ )($rootScope);
+ };
+ expect(func).not.toThrow();
+ });
+ });
+
it('should not throw exception when using "watch" as binding in Firefox', inject(
function($rootScope, $compile) {
$rootScope.watch = 'watch';
From 394dbcc2773e218b1eaa7b1f2b14a943ef78c8e0 Mon Sep 17 00:00:00 2001
From: Carl
Date: Thu, 27 Jul 2017 15:09:44 +0200
Subject: [PATCH 029/552] fix($httpParamSerializer): ignore functions
Closes #16133
---
src/ng/http.js | 2 +-
test/ng/httpSpec.js | 5 ++++-
2 files changed, 5 insertions(+), 2 deletions(-)
diff --git a/src/ng/http.js b/src/ng/http.js
index f2c597c452a7..a4e2094ab203 100644
--- a/src/ng/http.js
+++ b/src/ng/http.js
@@ -41,7 +41,7 @@ function $HttpParamSerializerProvider() {
if (!params) return '';
var parts = [];
forEachSorted(params, function(value, key) {
- if (value === null || isUndefined(value)) return;
+ if (value === null || isUndefined(value) || isFunction(value)) return;
if (isArray(value)) {
forEach(value, function(v) {
parts.push(encodeUriQuery(key) + '=' + encodeUriQuery(serializeValue(v)));
diff --git a/test/ng/httpSpec.js b/test/ng/httpSpec.js
index 5b21ee15a56b..7ed6993c4109 100644
--- a/test/ng/httpSpec.js
+++ b/test/ng/httpSpec.js
@@ -2344,7 +2344,6 @@ describe('$http param serializers', function() {
expect(defSer({someDate: new Date('2014-07-15T17:30:00.000Z')})).toEqual('someDate=2014-07-15T17:30:00.000Z');
expect(jqrSer({someDate: new Date('2014-07-15T17:30:00.000Z')})).toEqual('someDate=2014-07-15T17:30:00.000Z');
});
-
});
describe('default array serialization', function() {
@@ -2352,6 +2351,10 @@ describe('$http param serializers', function() {
it('should serialize arrays by repeating param name', function() {
expect(defSer({a: 'b', foo: ['bar', 'baz']})).toEqual('a=b&foo=bar&foo=baz');
});
+
+ it('should NOT serialize functions', function() {
+ expect(defSer({foo: 'foov', bar: function() {}})).toEqual('foo=foov');
+ });
});
describe('jquery array and objects serialization', function() {
From d91a6bdbc66098ff7bbf8fa435ae5652fe6a795f Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Tue, 18 Jul 2017 16:09:34 +0200
Subject: [PATCH 030/552] chore(docs-app): load example files based on active
deployment
---
docs/app/src/app.js | 1 +
docs/app/src/examples.js | 8 +++++++-
docs/config/index.js | 12 +++++++++++-
docs/config/processors/index-page.js | 10 ++++++++++
docs/config/services/deployments/debug.js | 1 +
docs/config/services/deployments/default.js | 1 +
docs/config/services/deployments/jquery.js | 1 +
docs/config/services/deployments/production.js | 1 +
.../templates/examples/runnableExample.template.html | 2 +-
9 files changed, 34 insertions(+), 3 deletions(-)
diff --git a/docs/app/src/app.js b/docs/app/src/app.js
index df6272b0bf0b..20e351ae97e1 100644
--- a/docs/app/src/app.js
+++ b/docs/app/src/app.js
@@ -14,6 +14,7 @@ angular.module('docsApp', [
'search',
'tutorials',
'versions',
+ 'deployment',
'ui.bootstrap.dropdown'
])
diff --git a/docs/app/src/examples.js b/docs/app/src/examples.js
index b5b21c5393fa..c9ffb20f016a 100644
--- a/docs/app/src/examples.js
+++ b/docs/app/src/examples.js
@@ -18,7 +18,13 @@ angular.module('examples', [])
return {
restrict: 'C',
scope : true,
- controller : ['$scope', function($scope) {
+ controller : ['$scope', 'DEPLOYMENT', function($scope, DEPLOYMENT) {
+ var exampleIndexFile = (DEPLOYMENT === 'default' ? 'index' : 'index-' + DEPLOYMENT) + '.html';
+
+ $scope.getExampleIndex = function(basePath) {
+ return basePath + '/' + exampleIndexFile;
+ };
+
$scope.setTab = function(index) {
var tab = $scope.tabs[index];
$scope.activeTabIndex = index;
diff --git a/docs/config/index.js b/docs/config/index.js
index ab5e45a3f8dc..e228fc34b72e 100644
--- a/docs/config/index.js
+++ b/docs/config/index.js
@@ -112,6 +112,10 @@ module.exports = new Package('angularjs', [
docTypes: ['indexPage'],
pathTemplate: '.',
outputPathTemplate: '${id}.html'
+ }, {
+ docTypes: ['deploymentData'],
+ pathTemplate: '.',
+ outputPathTemplate: 'js/${id}.js'
});
computePathsProcessor.pathTemplates.push({
@@ -125,8 +129,14 @@ module.exports = new Package('angularjs', [
outputPathTemplate: 'partials/${area}/${moduleName}/${groupType}.html'
});
+ computePathsProcessor.pathTemplates.push({
+ docTypes: ['example'],
+ pathTemplate: 'examples/${example.id}',
+ outputPathTemplate: 'examples/${example.id}/index${deploymentQualifier}.html'
+ });
+
computeIdsProcessor.idTemplates.push({
- docTypes: ['overview', 'tutorial', 'e2e-test', 'indexPage'],
+ docTypes: ['overview', 'tutorial', 'e2e-test', 'indexPage', 'deploymentData'],
getId: function(doc) { return doc.fileInfo.baseName; },
getAliases: function(doc) { return [doc.id]; }
});
diff --git a/docs/config/processors/index-page.js b/docs/config/processors/index-page.js
index 102e3f53db50..3a7c8d210e47 100644
--- a/docs/config/processors/index-page.js
+++ b/docs/config/processors/index-page.js
@@ -35,7 +35,17 @@ module.exports = function generateIndexPagesProcessor() {
indexDoc.id = 'index' + (deployment.name === 'default' ? '' : '-' + deployment.name);
+ var deploymentDoc = {
+ docType: 'deploymentData',
+ id: 'deployment-data-' + deployment.name,
+ template: 'angular-service.template.js',
+ ngModuleName: 'deployment',
+ serviceName: 'DEPLOYMENT',
+ serviceValue: deployment.name
+ };
+
docs.push(indexDoc);
+ docs.push(deploymentDoc);
});
}
};
diff --git a/docs/config/services/deployments/debug.js b/docs/config/services/deployments/debug.js
index 3581184c8e8e..6c61881232fc 100644
--- a/docs/config/services/deployments/debug.js
+++ b/docs/config/services/deployments/debug.js
@@ -26,6 +26,7 @@ module.exports = function debugDeployment(getVersion) {
'js/all-versions-data.js',
'js/pages-data.js',
'js/nav-data.js',
+ 'js/deployment-data-debug.js',
'js/docs.js'
],
stylesheets: [
diff --git a/docs/config/services/deployments/default.js b/docs/config/services/deployments/default.js
index 3abd0dd14c06..96f93f1d73b4 100644
--- a/docs/config/services/deployments/default.js
+++ b/docs/config/services/deployments/default.js
@@ -26,6 +26,7 @@ module.exports = function defaultDeployment(getVersion) {
'js/all-versions-data.js',
'js/pages-data.js',
'js/nav-data.js',
+ 'js/deployment-data-default.js',
'js/docs.min.js'
],
stylesheets: [
diff --git a/docs/config/services/deployments/jquery.js b/docs/config/services/deployments/jquery.js
index 4d4f64f6f416..cb4e1b0956fb 100644
--- a/docs/config/services/deployments/jquery.js
+++ b/docs/config/services/deployments/jquery.js
@@ -30,6 +30,7 @@ module.exports = function jqueryDeployment(getVersion) {
'js/all-versions-data.js',
'js/pages-data.js',
'js/nav-data.js',
+ 'js/deployment-data-jquery.js',
'js/docs.min.js'
],
stylesheets: [
diff --git a/docs/config/services/deployments/production.js b/docs/config/services/deployments/production.js
index 61d64b1f93d4..69a62ebba577 100644
--- a/docs/config/services/deployments/production.js
+++ b/docs/config/services/deployments/production.js
@@ -43,6 +43,7 @@ module.exports = function productionDeployment(getVersion) {
'https://code.angularjs.org/snapshot/docs/js/all-versions-data.js',
'js/pages-data.js',
'js/nav-data.js',
+ 'js/deployment-data-production.js',
'js/docs.min.js'
],
stylesheets: [
diff --git a/docs/config/templates/examples/runnableExample.template.html b/docs/config/templates/examples/runnableExample.template.html
index 26d689c10ad5..41a7a3502671 100644
--- a/docs/config/templates/examples/runnableExample.template.html
+++ b/docs/config/templates/examples/runnableExample.template.html
@@ -18,7 +18,7 @@
{% endfor %}
-
+
From 1b196332f5c76d809ce06b10263af7772c8072bb Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 21 Jul 2017 13:35:40 +0200
Subject: [PATCH 031/552] chore(docs-app): only copy relevant assets
This keeps the size of the docs-app build down.
Especially needed to keep the size of the generated build .zip
under 10MB, which is the limit for firebase / gcs https function transfers
---
docs/app/assets/css/docs.css | 34 +++++++++++++++++++
docs/config/services/deployments/debug.js | 1 -
docs/config/services/deployments/default.js | 3 +-
docs/config/services/deployments/jquery.js | 1 -
.../config/services/deployments/production.js | 3 +-
docs/gulpfile.js | 19 ++++++-----
6 files changed, 47 insertions(+), 14 deletions(-)
diff --git a/docs/app/assets/css/docs.css b/docs/app/assets/css/docs.css
index 77fcdbd400da..020b42a69b26 100644
--- a/docs/app/assets/css/docs.css
+++ b/docs/app/assets/css/docs.css
@@ -1,3 +1,37 @@
+@font-face {
+ font-family: 'Open Sans';
+ src: url("../components/open-sans-fontface-1.4.0/fonts/Regular/OpenSans-Regular.eot?v=1.1.0");
+ src: url("../components/open-sans-fontface-1.4.0/fonts/Regular/OpenSans-Regular.eot?#iefix&v=1.1.0") format("embedded-opentype"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Regular/OpenSans-Regular.woff?v=1.1.0") format("woff"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Regular/OpenSans-Regular.ttf?v=1.1.0") format("truetype"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Regular/OpenSans-Regular.svg?v=1.1.0#OpenSansBold") format("svg");
+ font-weight: normal;
+ font-style: normal;
+}
+
+@font-face {
+ font-family: 'Open Sans';
+ src: url("../components/open-sans-fontface-1.4.0/fonts/Semibold/OpenSans-Semibold.eot?v=1.1.0");
+ src: url("../components/open-sans-fontface-1.4.0/fonts/Semibold/OpenSans-Semibold.eot?#iefix&v=1.1.0") format("embedded-opentype"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Semibold/OpenSans-Semibold.woff?v=1.1.0") format("woff"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Semibold/OpenSans-Semibold.ttf?v=1.1.0") format("truetype"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Semibold/OpenSans-Semibold.svg?v=1.1.0#OpenSansBold") format("svg");
+ font-weight: 600;
+ font-style: normal;
+}
+
+
+@font-face {
+ font-family: 'Open Sans';
+ src: url("../components/open-sans-fontface-1.4.0/fonts/Bold/OpenSans-Bold.eot?v=1.1.0");
+ src: url("../components/open-sans-fontface-1.4.0/fonts/Bold/OpenSans-Bold.eot?#iefix&v=1.1.0") format("embedded-opentype"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Bold/OpenSans-Bold.woff?v=1.1.0") format("woff"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Bold/OpenSans-Bold.ttf?v=1.1.0") format("truetype"),
+ url("../components/open-sans-fontface-1.4.0/fonts/Bold/OpenSans-Bold.svg?v=1.1.0#OpenSansBold") format("svg");
+ font-weight: bold;
+ font-style: normal;
+}
+
html, body {
position: relative;
height: 100%;
diff --git a/docs/config/services/deployments/debug.js b/docs/config/services/deployments/debug.js
index 6c61881232fc..b2b9da22b842 100644
--- a/docs/config/services/deployments/debug.js
+++ b/docs/config/services/deployments/debug.js
@@ -31,7 +31,6 @@ module.exports = function debugDeployment(getVersion) {
],
stylesheets: [
'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.css',
- 'components/open-sans-fontface-' + getVersion('open-sans-fontface') + '/open-sans.css',
'css/prettify-theme.css',
'css/angular-topnav.css',
'css/docs.css',
diff --git a/docs/config/services/deployments/default.js b/docs/config/services/deployments/default.js
index 96f93f1d73b4..eb1778a91903 100644
--- a/docs/config/services/deployments/default.js
+++ b/docs/config/services/deployments/default.js
@@ -17,7 +17,7 @@ module.exports = function defaultDeployment(getVersion) {
'../angular-sanitize.min.js',
'../angular-touch.min.js',
'../angular-animate.min.js',
- 'components/marked-' + getVersion('marked') + '/lib/marked.js',
+ 'components/marked-' + getVersion('marked') + '/marked.min.js',
'js/angular-bootstrap/dropdown-toggle.min.js',
'components/lunr-' + getVersion('lunr') + '/lunr.min.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
@@ -31,7 +31,6 @@ module.exports = function defaultDeployment(getVersion) {
],
stylesheets: [
'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css',
- 'components/open-sans-fontface-' + getVersion('open-sans-fontface') + '/open-sans.css',
'css/prettify-theme.css',
'css/angular-topnav.css',
'css/docs.css',
diff --git a/docs/config/services/deployments/jquery.js b/docs/config/services/deployments/jquery.js
index cb4e1b0956fb..592de9dffabb 100644
--- a/docs/config/services/deployments/jquery.js
+++ b/docs/config/services/deployments/jquery.js
@@ -35,7 +35,6 @@ module.exports = function jqueryDeployment(getVersion) {
],
stylesheets: [
'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css',
- 'components/open-sans-fontface-' + getVersion('open-sans-fontface') + '/open-sans.css',
'css/prettify-theme.css',
'css/angular-topnav.css',
'css/docs.css',
diff --git a/docs/config/services/deployments/production.js b/docs/config/services/deployments/production.js
index 69a62ebba577..859c490388ba 100644
--- a/docs/config/services/deployments/production.js
+++ b/docs/config/services/deployments/production.js
@@ -34,7 +34,7 @@ module.exports = function productionDeployment(getVersion) {
cdnUrl + '/angular-sanitize.min.js',
cdnUrl + '/angular-touch.min.js',
cdnUrl + '/angular-animate.min.js',
- 'components/marked-' + getVersion('marked') + '/lib/marked.js',
+ 'components/marked-' + getVersion('marked') + '/marked.min.js',
'js/angular-bootstrap/dropdown-toggle.min.js',
'components/lunr-' + getVersion('lunr') + '/lunr.min.js',
'components/google-code-prettify-' + getVersion('google-code-prettify') + '/src/prettify.js',
@@ -48,7 +48,6 @@ module.exports = function productionDeployment(getVersion) {
],
stylesheets: [
'components/bootstrap-' + getVersion('bootstrap') + '/css/bootstrap.min.css',
- 'components/open-sans-fontface-' + getVersion('open-sans-fontface') + '/open-sans.css',
'css/prettify-theme.css',
'css/angular-topnav.css',
'css/docs.css',
diff --git a/docs/gulpfile.js b/docs/gulpfile.js
index 1c092f431483..e45c504b6537 100644
--- a/docs/gulpfile.js
+++ b/docs/gulpfile.js
@@ -52,13 +52,14 @@ var getMergedEslintConfig = function(filepath) {
};
};
-var copyComponent = function(component, pattern, sourceFolder, packageFile) {
+var copyComponent = function(component, pattern, base, sourceFolder, packageFile) {
pattern = pattern || '/**/*';
+ base = base || '';
sourceFolder = sourceFolder || '../node_modules';
packageFile = packageFile || 'package.json';
var version = require(path.resolve(sourceFolder, component, packageFile)).version;
return gulp
- .src(sourceFolder + '/' + component + pattern)
+ .src(sourceFolder + '/' + component + pattern, {base: sourceFolder + '/' + component + '/' + base})
.pipe(gulp.dest(outputFolder + '/components/' + component + '-' + version));
};
@@ -96,12 +97,14 @@ gulp.task('assets', function() {
.pipe(gulp.dest(outputFolder));
}
})),
- copyComponent('bootstrap', '/dist/**/*'),
- copyComponent('open-sans-fontface'),
- copyComponent('lunr', '/*.js'),
- copyComponent('google-code-prettify'),
- copyComponent('jquery', '/dist/*.js'),
- copyComponent('marked', '/**/*.js')
+ copyComponent('bootstrap', '/dist/css/bootstrap?(.min).css', 'dist'),
+ copyComponent('bootstrap', '/dist/fonts/*', 'dist'),
+ copyComponent('open-sans-fontface', '/fonts/{Regular,Semibold,Bold}/*'),
+ copyComponent('lunr', '/lunr?(.min).js'),
+ copyComponent('google-code-prettify', '/**/{lang-css,prettify}.js'),
+ copyComponent('jquery', '/dist/jquery.js', 'dist'),
+ copyComponent('marked', '/lib/marked.js'),
+ copyComponent('marked', '/marked.min.js')
);
});
From 9936e570ea91098c7c6ea59809468224d07296ec Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Tue, 18 Jul 2017 17:28:06 +0200
Subject: [PATCH 032/552] chore(travis): skip build on deployment job when from
Pull Request
---
scripts/travis/build.sh | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh
index bc71d1d836be..efcc16d6e08e 100755
--- a/scripts/travis/build.sh
+++ b/scripts/travis/build.sh
@@ -46,8 +46,13 @@ case "$JOB" in
grunt test:travis-protractor --specs="$TARGET_SPECS"
;;
"deploy")
- grunt package
- grunt compress:firebaseCodeDeploy
+ # we never deploy on Pull requests, so it's safe to skip the build here
+ if [[ $TRAVIS_PULL_REQUEST != 'false' ]]; then
+ grunt package
+ grunt compress:firebaseCodeDeploy
+ else
+ echo "Skipping build because Travis has been triggered by Pull Request"
+ fi
;;
*)
echo "Unknown job type. Please set JOB=ci-checks, JOB=unit, JOB=deploy or JOB=e2e-*."
From 84061c29547e93bb9da604c147420dc8d4712f3f Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Wed, 19 Jul 2017 14:05:35 +0200
Subject: [PATCH 033/552] chore(code.angularjs): delete old zip files on
snapshot
---
.../functions/index.js | 34 +++++++++++++++++++
1 file changed, 34 insertions(+)
diff --git a/scripts/code.angularjs.org-firebase/functions/index.js b/scripts/code.angularjs.org-firebase/functions/index.js
index 18fdd6c7a44a..de1b5dba96e3 100644
--- a/scripts/code.angularjs.org-firebase/functions/index.js
+++ b/scripts/code.angularjs.org-firebase/functions/index.js
@@ -72,4 +72,38 @@ function sendStoredFile(request, response) {
}
}
+function deleteOldSnapshotZip(event) {
+ const object = event.data;
+
+ const bucketId = object.bucket;
+ const filePath = object.name;
+ const contentType = object.contentType;
+
+ const bucket = gcs.bucket(bucketId);
+
+ if (event.eventType === 'providers/cloud.storage/eventTypes/object.change' &&
+ contentType === 'application/zip' &&
+ filePath.startsWith('snapshot/')
+ ) {
+
+ bucket.getFiles({
+ prefix: 'snapshot/',
+ delimiter: '/',
+ autoPaginate: false
+ }).then(function(data) {
+ const files = data[0];
+
+ const oldZipFiles = files.filter(file => {
+ return file.metadata.name !== filePath && file.metadata.contentType === 'application/zip';
+ });
+
+ oldZipFiles.forEach(function(file) {
+ file.delete();
+ });
+
+ });
+ }
+}
+
exports.sendStoredFile = functions.https.onRequest(sendStoredFile);
+exports.deleteOldSnapshotZip = functions.storage.object().onChange(deleteOldSnapshotZip);
From 0179c70d02211328ebc4cf6dc4839e7ae8522965 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 21 Jul 2017 13:37:28 +0200
Subject: [PATCH 034/552] chore(code.angularjs): enable directory listings
---
.../functions/index.js | 137 +++++++++++++++---
.../public/index.html | 10 --
2 files changed, 117 insertions(+), 30 deletions(-)
delete mode 100644 scripts/code.angularjs.org-firebase/public/index.html
diff --git a/scripts/code.angularjs.org-firebase/functions/index.js b/scripts/code.angularjs.org-firebase/functions/index.js
index de1b5dba96e3..5b8b12e3002e 100644
--- a/scripts/code.angularjs.org-firebase/functions/index.js
+++ b/scripts/code.angularjs.org-firebase/functions/index.js
@@ -5,7 +5,6 @@ const gcs = require('@google-cloud/storage')();
const path = require('path');
const gcsBucketId = `${process.env.GCLOUD_PROJECT}.appspot.com`;
-const LOCAL_TMP_FOLDER = '/tmp/';
const BROWSER_CACHE_DURATION = 300;
const CDN_CACHE_DURATION = 600;
@@ -22,7 +21,6 @@ function sendStoredFile(request, response) {
const bucket = gcs.bucket(gcsBucketId);
let downloadSource;
- let downloadDestination;
let fileName;
if (isDocsPath && filePathSegments.length === 2) {
@@ -32,43 +30,142 @@ function sendStoredFile(request, response) {
fileName = lastSegment;
}
+ if (!fileName) {
+ //Root
+ return getDirectoryListing('/').catch(sendErrorResponse);
+ }
+
downloadSource = path.join.apply(null, filePathSegments);
- downloadDestination = `${LOCAL_TMP_FOLDER}${fileName}`;
- downloadAndSend(downloadSource, downloadDestination).catch(error => {
+ downloadAndSend(downloadSource).catch(error => {
if (isDocsPath && error.code === 404) {
fileName = 'index.html';
filePathSegments = [version, 'docs', fileName];
downloadSource = path.join.apply(null, filePathSegments);
- downloadDestination = `${LOCAL_TMP_FOLDER}${fileName}`;
- return downloadAndSend(downloadSource, downloadDestination);
+ return downloadAndSend(downloadSource);
}
return Promise.reject(error);
}).catch(error => {
- let message = 'General error';
+
+ // If file not found, try the path as a directory
+ return error.code === 404 ? getDirectoryListing(request.path.slice(1)) : Promise.reject(error);
+ }).catch(sendErrorResponse);
+
+ function downloadAndSend(downloadSource) {
+
+ const file = bucket.file(downloadSource);
+
+ return file.getMetadata().then(data => {
+ return new Promise((resolve, reject) => {
+
+ const readStream = file.createReadStream()
+ .on('error', error => {
+ reject(error);
+ })
+ .on('response', () => {
+ resolve(response);
+ });
+
+ response
+ .status(200)
+ .set({
+ 'Content-Type': data[0].contentType,
+ 'Cache-Control': `public, max-age=${BROWSER_CACHE_DURATION}, s-maxage=${CDN_CACHE_DURATION}`
+ });
+
+ readStream.pipe(response);
+ });
+
+ });
+ }
+
+ function sendErrorResponse(error) {
+ let code = 500;
+ let message = `General error. Please try again later.
+ If the error persists, please create an issue in the
+ AngularJS Github repository`;
+
if (error.code === 404) {
- if (fileName.split('.').length === 1) {
- message = 'Directory listing is not supported';
- } else {
- message = 'File not found';
- }
+ message = 'File or directory not found';
+ code = 404;
}
- return response.status(error.code).send(message);
- });
+ return response.status(code).send(message);
+ }
+
+ function getDirectoryListing(path) {
+ if (!path.endsWith('/')) path += '/';
- function downloadAndSend(downloadSource, downloadDestination) {
- return bucket.file(downloadSource).download({
- destination: downloadDestination
- }).then(() => {
- return response.status(200)
+ const getFilesOptions = {
+ delimiter: '/',
+ autoPaginate: false
+ };
+
+ if (path !== '/') getFilesOptions.prefix = path;
+
+ let fileList = [];
+ let directoryList = [];
+
+ return getContent(getFilesOptions).then(() => {
+ let contentList = '';
+
+ directoryList.forEach(directoryPath => {
+ const dirName = directoryPath.split('/').reverse()[1];
+ contentList += `${dirName}/ `;
+ });
+
+ fileList.forEach(file => {
+ const fileName = file.metadata.name.split('/').pop();
+ contentList += `${fileName} `;
+ });
+
+ // A trailing slash in the base creates correct relative links when the url is accessed
+ // without trailing slash
+ const base = request.originalUrl.endsWith('/') ? request.originalUrl : request.originalUrl + '/';
+
+ let directoryListing = `
+
+
Index of ${path}
+
+
${contentList}
`;
+
+ return response
+ .status(200)
.set({
'Cache-Control': `public, max-age=${BROWSER_CACHE_DURATION}, s-maxage=${CDN_CACHE_DURATION}`
})
- .sendFile(downloadDestination);
+ .send(directoryListing);
});
+
+ function getContent(options) {
+ return bucket.getFiles(options).then(data => {
+ const files = data[0];
+ const nextQuery = data[1];
+ const apiResponse = data[2];
+
+ if (!files.length && (!apiResponse || !apiResponse.prefixes)) {
+ return Promise.reject({
+ code: 404
+ });
+ }
+
+ fileList = fileList.concat(files);
+
+ if (apiResponse && apiResponse.prefixes) {
+ directoryList = directoryList.concat(apiResponse.prefixes);
+ }
+
+ if (nextQuery) {
+ // If the results are paged, get the next page
+ return getContent(nextQuery);
+ }
+
+ return true;
+ });
+
+ }
}
}
diff --git a/scripts/code.angularjs.org-firebase/public/index.html b/scripts/code.angularjs.org-firebase/public/index.html
deleted file mode 100644
index fe8e94ca62c8..000000000000
--- a/scripts/code.angularjs.org-firebase/public/index.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
-
- AngularJS
-
-
-
-
From aee5d02cb789e178f3f80f95cdabea38e0090501 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Mon, 31 Jul 2017 23:31:39 +0200
Subject: [PATCH 035/552] chore(travis): actually skip build on deployment job
when from Pull Request
---
scripts/travis/build.sh | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/travis/build.sh b/scripts/travis/build.sh
index efcc16d6e08e..f7b33c3129da 100755
--- a/scripts/travis/build.sh
+++ b/scripts/travis/build.sh
@@ -47,7 +47,7 @@ case "$JOB" in
;;
"deploy")
# we never deploy on Pull requests, so it's safe to skip the build here
- if [[ $TRAVIS_PULL_REQUEST != 'false' ]]; then
+ if [[ "$TRAVIS_PULL_REQUEST" == "false" ]]; then
grunt package
grunt compress:firebaseCodeDeploy
else
From 3650723501ca5f50003192bc4f94c77cef711e1a Mon Sep 17 00:00:00 2001
From: andypotts
Date: Sun, 30 Jul 2017 17:53:57 +0100
Subject: [PATCH 036/552] docs(guide/concepts): simplify currency exchange API
example (YQL --> Fixer.io)
Fixes #16130
Closes #16137
---
docs/content/guide/concepts.ngdoc | 19 +++++--------------
docs/content/guide/security.ngdoc | 3 +--
2 files changed, 6 insertions(+), 16 deletions(-)
diff --git a/docs/content/guide/concepts.ngdoc b/docs/content/guide/concepts.ngdoc
index cc64ddb92e0e..b7ed900c5ff5 100644
--- a/docs/content/guide/concepts.ngdoc
+++ b/docs/content/guide/concepts.ngdoc
@@ -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
{@link services service}, 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 Yahoo Finance API, without changing the controller.
+from the web, e.g. by calling the [Fixer.io](http://fixer.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 Yahoo Finance API.
+Let's finish our example by fetching the exchange rates from the [Fixer.io](http://fixer.io) exchange rate API.
The following example shows how this is done with AngularJS:
@@ -323,10 +323,6 @@ The following example shows how this is done with AngularJS:
angular.module('finance3', [])
.factory('currencyConverter', ['$http', function($http) {
- var YAHOO_FINANCE_URL_PATTERN =
- '//query.yahooapis.com/v1/public/yql?q=select * from ' +
- 'yahoo.finance.xchange where pair in ("PAIRS")&format=json&' +
- 'env=store://datatables.org/alltableswithkeys';
var currencies = ['USD', 'EUR', 'CNY'];
var usdToForeignRates = {};
@@ -335,15 +331,10 @@ The following example shows how this is done with AngularJS:
};
var refresh = function() {
- var url = YAHOO_FINANCE_URL_PATTERN.
- replace('PAIRS', 'USD' + currencies.join('","USD'));
+ var url = 'https://api.fixer.io/latest?base=USD&symbols=' + currencies.join(",");
return $http.get(url).then(function(response) {
- var newUsdToForeignRates = {};
- angular.forEach(response.data.query.results.rate, function(rate) {
- var currency = rate.id.substring(3,6);
- newUsdToForeignRates[currency] = window.parseFloat(rate.Rate);
- });
- usdToForeignRates = newUsdToForeignRates;
+ usdToForeignRates = response.data.rates;
+ usdToForeignRates['USD'] = 1;
});
};
diff --git a/docs/content/guide/security.ngdoc b/docs/content/guide/security.ngdoc
index e4236de03d07..2a8e6b6feda8 100644
--- a/docs/content/guide/security.ngdoc
+++ b/docs/content/guide/security.ngdoc
@@ -100,8 +100,7 @@ Protection from JSON Hijacking is provided if the server prefixes all JSON reque
AngularJS will automatically strip the prefix before processing it as JSON.
For more information please visit {@link $http#json-vulnerability-protection JSON Hijacking Protection}.
-Bear in mind that calling `$http.jsonp`, like in [our Yahoo! finance example](https://docs.angularjs.org/guide/concepts#accessing-the-backend),
-gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
+Bear in mind that calling `$http.jsonp` gives the remote server (and, if the request is not secured, any Man-in-the-Middle attackers)
instant remote code execution in your application: the result of these requests is handed off
to the browser as regular `');
- }
-
- function addCSS(path) {
- window.document.write('');
- }
-
- window.onload = function() {
- try {
- if (previousOnLoad) previousOnLoad();
- } catch (e) { /* empty */ }
- angular.scenario.setUpAndRun({});
- };
-
- addCSS('../../css/angular-scenario.css');
- addScript('../../lib/jquery/jquery.js');
- window.document.write(
- ''
- );
- addScript('../angular-bootstrap.js');
-
- addScript('Scenario.js');
- addScript('Application.js');
- addScript('Describe.js');
- addScript('Future.js');
- addScript('Runner.js');
- addScript('SpecRunner.js');
- addScript('dsl.js');
- addScript('matchers.js');
- addScript('ObjectModel.js');
- addScript('output/Html.js');
- addScript('output/Json.js');
- addScript('output/Object.js');
- addScript('output/Xml.js');
-
- // Create the runner (which also sets up the global API)
- window.document.write(
- '');
-
-})(window.onload);
diff --git a/src/ngScenario/angular.prefix b/src/ngScenario/angular.prefix
deleted file mode 100644
index 3ccd4537f775..000000000000
--- a/src/ngScenario/angular.prefix
+++ /dev/null
@@ -1,7 +0,0 @@
-/**
- * @license AngularJS v"NG_VERSION_FULL"
- * (c) 2010-2017 Google, Inc. http://angularjs.org
- * License: MIT
- */
-(function(window){
- var _jQuery = window.jQuery.noConflict(true);
diff --git a/src/ngScenario/angular.suffix b/src/ngScenario/angular.suffix
deleted file mode 100644
index 5c89e61036f3..000000000000
--- a/src/ngScenario/angular.suffix
+++ /dev/null
@@ -1,22 +0,0 @@
-bindJQuery();
-publishExternalAPI(angular);
-
-var $runner = new angular.scenario.Runner(window),
- scripts = window.document.getElementsByTagName('script'),
- script = scripts[scripts.length - 1],
- config = {};
-
-angular.forEach(script.attributes, function(attr) {
- var match = attr.name.match(/ng[:\-](.*)/);
- if (match) {
- config[match[1]] = attr.value || true;
- }
-});
-
-if (config.autotest) {
- JQLite(function() {
- angular.scenario.setUpAndRun(config);
- });
-}
-})(window);
-
diff --git a/src/ngScenario/dsl.js b/src/ngScenario/dsl.js
deleted file mode 100644
index 3849c4515aff..000000000000
--- a/src/ngScenario/dsl.js
+++ /dev/null
@@ -1,484 +0,0 @@
-'use strict';
-
-/* eslint-disable no-invalid-this */
-
-/**
- * Shared DSL statements that are useful to all scenarios.
- */
-
- /**
- * Usage:
- * pause() pauses until you call resume() in the console
- */
-angular.scenario.dsl('pause', function() {
- return function() {
- return this.addFuture('pausing for you to resume', function(done) {
- this.emit('InteractivePause', this.spec, this.step);
- this.$window.resume = function() { done(); };
- });
- };
-});
-
-/**
- * Usage:
- * sleep(seconds) pauses the test for specified number of seconds
- */
-angular.scenario.dsl('sleep', function() {
- return function(time) {
- return this.addFuture('sleep for ' + time + ' seconds', function(done) {
- this.$window.setTimeout(function() { done(null, time * 1000); }, time * 1000);
- });
- };
-});
-
-/**
- * Usage:
- * browser().navigateTo(url) Loads the url into the frame
- * browser().navigateTo(url, fn) where fn(url) is called and returns the URL to navigate to
- * browser().reload() refresh the page (reload the same URL)
- * browser().window.href() window.location.href
- * browser().window.path() window.location.pathname
- * browser().window.search() window.location.search
- * browser().window.hash() window.location.hash without # prefix
- * browser().location().url() see ng.$location#url
- * browser().location().path() see ng.$location#path
- * browser().location().search() see ng.$location#search
- * browser().location().hash() see ng.$location#hash
- */
-angular.scenario.dsl('browser', function() {
- var chain = {};
-
- chain.navigateTo = function(url, delegate) {
- var application = this.application;
- return this.addFuture('browser navigate to \'' + url + '\'', function(done) {
- if (delegate) {
- url = delegate.call(this, url);
- }
- application.navigateTo(url, function() {
- done(null, url);
- }, done);
- });
- };
-
- chain.reload = function() {
- var application = this.application;
- return this.addFutureAction('browser reload', function($window, $document, done) {
- var href = $window.location.href;
- application.navigateTo(href, function() {
- done(null, href);
- }, done);
- });
- };
-
- chain.window = function() {
- var api = {};
-
- api.href = function() {
- return this.addFutureAction('window.location.href', function($window, $document, done) {
- done(null, $window.location.href);
- });
- };
-
- api.path = function() {
- return this.addFutureAction('window.location.path', function($window, $document, done) {
- done(null, $window.location.pathname);
- });
- };
-
- api.search = function() {
- return this.addFutureAction('window.location.search', function($window, $document, done) {
- done(null, $window.location.search);
- });
- };
-
- api.hash = function() {
- return this.addFutureAction('window.location.hash', function($window, $document, done) {
- done(null, $window.location.hash.replace('#', ''));
- });
- };
-
- return api;
- };
-
- chain.location = function() {
- var api = {};
-
- api.url = function() {
- return this.addFutureAction('$location.url()', function($window, $document, done) {
- done(null, $document.injector().get('$location').url());
- });
- };
-
- api.path = function() {
- return this.addFutureAction('$location.path()', function($window, $document, done) {
- done(null, $document.injector().get('$location').path());
- });
- };
-
- api.search = function() {
- return this.addFutureAction('$location.search()', function($window, $document, done) {
- done(null, $document.injector().get('$location').search());
- });
- };
-
- api.hash = function() {
- return this.addFutureAction('$location.hash()', function($window, $document, done) {
- done(null, $document.injector().get('$location').hash());
- });
- };
-
- return api;
- };
-
- return function() {
- return chain;
- };
-});
-
-/**
- * Usage:
- * expect(future).{matcher} where matcher is one of the matchers defined
- * with angular.scenario.matcher
- *
- * ex. expect(binding("name")).toEqual("Elliott")
- */
-angular.scenario.dsl('expect', function() {
- var chain = angular.extend({}, angular.scenario.matcher);
-
- chain.not = function() {
- this.inverse = true;
- return chain;
- };
-
- return function(future) {
- this.future = future;
- return chain;
- };
-});
-
-/**
- * Usage:
- * using(selector, label) scopes the next DSL element selection
- *
- * ex.
- * using('#foo', "'Foo' text field").input('bar')
- */
-angular.scenario.dsl('using', function() {
- return function(selector, label) {
- this.selector = _jQuery.trim((this.selector || '') + ' ' + selector);
- if (angular.isString(label) && label.length) {
- this.label = label + ' ( ' + this.selector + ' )';
- } else {
- this.label = this.selector;
- }
- return this.dsl;
- };
-});
-
-/**
- * Usage:
- * binding(name) returns the value of the first matching binding
- */
-angular.scenario.dsl('binding', function() {
- return function(name) {
- return this.addFutureAction('select binding \'' + name + '\'',
- function($window, $document, done) {
- var values = $document.elements().bindings($window.angular.element, name);
- if (!values.length) {
- return done('Binding selector \'' + name + '\' did not match.');
- }
- done(null, values[0]);
- });
- };
-});
-
-/**
- * Usage:
- * input(name).enter(value) enters value in input with specified name
- * input(name).check() checks checkbox
- * input(name).select(value) selects the radio button with specified name/value
- * input(name).val() returns the value of the input.
- */
-angular.scenario.dsl('input', function() {
- var chain = {};
-
- // Support: IE 9-11 only
- // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have
- // it. In particular the event is not fired when backspace or delete key are pressed or
- // when cut operation is performed.
- // IE10+ implements 'input' event but it erroneously fires under various situations,
- // e.g. when placeholder changes, or a form is focused.
- var supportInputEvent = 'oninput' in window.document.createElement('div') && !msie;
-
- chain.enter = function(value, event) {
- return this.addFutureAction('input \'' + this.name + '\' enter \'' + value + '\'',
- function($window, $document, done) {
- var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
- input.val(value);
- input.trigger(event || (supportInputEvent ? 'input' : 'change'));
- done();
- });
- };
-
- chain.check = function() {
- return this.addFutureAction('checkbox \'' + this.name + '\' toggle',
- function($window, $document, done) {
- var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':checkbox');
- input.trigger('click');
- done();
- });
- };
-
- chain.select = function(value) {
- return this.addFutureAction('radio button \'' + this.name + '\' toggle \'' + value + '\'',
- function($window, $document, done) {
- var input = $document.
- elements('[ng\\:model="$1"][value="$2"]', this.name, value).filter(':radio');
- input.trigger('click');
- done();
- });
- };
-
- chain.val = function() {
- return this.addFutureAction('return input val', function($window, $document, done) {
- var input = $document.elements('[ng\\:model="$1"]', this.name).filter(':input');
- done(null,input.val());
- });
- };
-
- return function(name) {
- this.name = name;
- return chain;
- };
-});
-
-
-/**
- * Usage:
- * repeater('#products table', 'Product List').count() number of rows
- * repeater('#products table', 'Product List').row(1) all bindings in row as an array
- * repeater('#products table', 'Product List').column('product.name') all values across all rows
- * in an array
- */
-angular.scenario.dsl('repeater', function() {
- var chain = {};
-
- chain.count = function() {
- return this.addFutureAction('repeater \'' + this.label + '\' count',
- function($window, $document, done) {
- try {
- done(null, $document.elements().length);
- } catch (e) {
- done(null, 0);
- }
- });
- };
-
- chain.column = function(binding) {
- return this.addFutureAction('repeater \'' + this.label + '\' column \'' + binding + '\'',
- function($window, $document, done) {
- done(null, $document.elements().bindings($window.angular.element, binding));
- });
- };
-
- chain.row = function(index) {
- return this.addFutureAction('repeater \'' + this.label + '\' row \'' + index + '\'',
- function($window, $document, done) {
- var matches = $document.elements().slice(index, index + 1);
- if (!matches.length) {
- return done('row ' + index + ' out of bounds');
- }
- done(null, matches.bindings($window.angular.element));
- });
- };
-
- return function(selector, label) {
- this.dsl.using(selector, label);
- return chain;
- };
-});
-
-/**
- * Usage:
- * select(name).option('value') select one option
- * select(name).options('value1', 'value2', ...) select options from a multi select
- */
-angular.scenario.dsl('select', function() {
- var chain = {};
-
- chain.option = function(value) {
- return this.addFutureAction('select \'' + this.name + '\' option \'' + value + '\'',
- function($window, $document, done) {
- var select = $document.elements('select[ng\\:model="$1"]', this.name);
- var option = select.find('option[value="' + value + '"]');
- if (option.length) {
- select.val(value);
- } else {
- option = select.find('option').filter(function() {
- return _jQuery(this).text() === value;
- });
- if (!option.length) {
- option = select.find('option:contains("' + value + '")');
- }
- if (option.length) {
- select.val(option.val());
- } else {
- return done('option \'' + value + '\' not found');
- }
- }
- select.trigger('change');
- done();
- });
- };
-
- chain.options = function() {
- var values = arguments;
- return this.addFutureAction('select \'' + this.name + '\' options \'' + values + '\'',
- function($window, $document, done) {
- var select = $document.elements('select[multiple][ng\\:model="$1"]', this.name);
- select.val(values);
- select.trigger('change');
- done();
- });
- };
-
- return function(name) {
- this.name = name;
- return chain;
- };
-});
-
-/**
- * Usage:
- * element(selector, label).count() get the number of elements that match selector
- * element(selector, label).click() clicks an element
- * element(selector, label).mouseover() mouseover an element
- * element(selector, label).mousedown() mousedown an element
- * element(selector, label).mouseup() mouseup an element
- * element(selector, label).query(fn) executes fn(selectedElements, done)
- * element(selector, label).{method}() gets the value (as defined by jQuery, ex. val)
- * element(selector, label).{method}(value) sets the value (as defined by jQuery, ex. val)
- * element(selector, label).{method}(key) gets the value (as defined by jQuery, ex. attr)
- * element(selector, label).{method}(key, value) sets the value (as defined by jQuery, ex. attr)
- */
-angular.scenario.dsl('element', function() {
- var KEY_VALUE_METHODS = ['attr', 'css', 'prop'];
- var VALUE_METHODS = [
- 'val', 'text', 'html', 'height', 'innerHeight', 'outerHeight', 'width',
- 'innerWidth', 'outerWidth', 'position', 'scrollLeft', 'scrollTop', 'offset'
- ];
- var chain = {};
-
- chain.count = function() {
- return this.addFutureAction('element \'' + this.label + '\' count',
- function($window, $document, done) {
- try {
- done(null, $document.elements().length);
- } catch (e) {
- done(null, 0);
- }
- });
- };
-
- chain.click = function() {
- return this.addFutureAction('element \'' + this.label + '\' click',
- function($window, $document, done) {
- var elements = $document.elements();
- var href = elements.attr('href');
- var eventProcessDefault = elements.trigger('click')[0];
-
- if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) {
- this.application.navigateTo(href, function() {
- done();
- }, done);
- } else {
- done();
- }
- });
- };
-
- chain.dblclick = function() {
- return this.addFutureAction('element \'' + this.label + '\' dblclick',
- function($window, $document, done) {
- var elements = $document.elements();
- var href = elements.attr('href');
- var eventProcessDefault = elements.trigger('dblclick')[0];
-
- if (href && elements[0].nodeName.toLowerCase() === 'a' && eventProcessDefault) {
- this.application.navigateTo(href, function() {
- done();
- }, done);
- } else {
- done();
- }
- });
- };
-
- chain.mouseover = function() {
- return this.addFutureAction('element \'' + this.label + '\' mouseover',
- function($window, $document, done) {
- var elements = $document.elements();
- elements.trigger('mouseover');
- done();
- });
- };
-
- chain.mousedown = function() {
- return this.addFutureAction('element \'' + this.label + '\' mousedown',
- function($window, $document, done) {
- var elements = $document.elements();
- elements.trigger('mousedown');
- done();
- });
- };
-
- chain.mouseup = function() {
- return this.addFutureAction('element \'' + this.label + '\' mouseup',
- function($window, $document, done) {
- var elements = $document.elements();
- elements.trigger('mouseup');
- done();
- });
- };
-
- chain.query = function(fn) {
- return this.addFutureAction('element ' + this.label + ' custom query',
- function($window, $document, done) {
- fn.call(this, $document.elements(), done);
- });
- };
-
- angular.forEach(KEY_VALUE_METHODS, function(methodName) {
- chain[methodName] = function(name, value) {
- var args = arguments,
- futureName = (args.length === 1)
- ? 'element \'' + this.label + '\' get ' + methodName + ' \'' + name + '\''
- : 'element \'' + this.label + '\' set ' + methodName + ' \'' + name + '\' to \'' +
- value + '\'';
-
- return this.addFutureAction(futureName, function($window, $document, done) {
- var element = $document.elements();
- done(null, element[methodName].apply(element, args));
- });
- };
- });
-
- angular.forEach(VALUE_METHODS, function(methodName) {
- chain[methodName] = function(value) {
- var args = arguments,
- futureName = (args.length === 0)
- ? 'element \'' + this.label + '\' ' + methodName
- : 'element \'' + this.label + '\' set ' + methodName + ' to \'' + value + '\'';
-
- return this.addFutureAction(futureName, function($window, $document, done) {
- var element = $document.elements();
- done(null, element[methodName].apply(element, args));
- });
- };
- });
-
- return function(selector, label) {
- this.dsl.using(selector, label);
- return chain;
- };
-});
diff --git a/src/ngScenario/matchers.js b/src/ngScenario/matchers.js
deleted file mode 100644
index f2dea6f1de40..000000000000
--- a/src/ngScenario/matchers.js
+++ /dev/null
@@ -1,45 +0,0 @@
-'use strict';
-
-/**
- * Matchers for implementing specs. Follows the Jasmine spec conventions.
- */
-
-angular.scenario.matcher('toEqual', /** @this */ function(expected) {
- return angular.equals(this.actual, expected);
-});
-
-angular.scenario.matcher('toBe', /** @this */ function(expected) {
- return this.actual === expected;
-});
-
-angular.scenario.matcher('toBeDefined', /** @this */ function() {
- return angular.isDefined(this.actual);
-});
-
-angular.scenario.matcher('toBeTruthy', /** @this */ function() {
- return this.actual;
-});
-
-angular.scenario.matcher('toBeFalsy', /** @this */ function() {
- return !this.actual;
-});
-
-angular.scenario.matcher('toMatch', /** @this */ function(expected) {
- return new RegExp(expected).test(this.actual);
-});
-
-angular.scenario.matcher('toBeNull', /** @this */ function() {
- return this.actual === null;
-});
-
-angular.scenario.matcher('toContain', /** @this */ function(expected) {
- return includes(this.actual, expected);
-});
-
-angular.scenario.matcher('toBeLessThan', /** @this */ function(expected) {
- return this.actual < expected;
-});
-
-angular.scenario.matcher('toBeGreaterThan', /** @this */ function(expected) {
- return this.actual > expected;
-});
diff --git a/src/ngScenario/output/Html.js b/src/ngScenario/output/Html.js
deleted file mode 100644
index dcd87a3f76b5..000000000000
--- a/src/ngScenario/output/Html.js
+++ /dev/null
@@ -1,171 +0,0 @@
-'use strict';
-
-/**
- * User Interface for the Scenario Runner.
- *
- * TODO(esprehn): This should be refactored now that ObjectModel exists
- * to use AngularJS bindings for the UI.
- */
-angular.scenario.output('html', function(context, runner, model) {
- var specUiMap = {},
- lastStepUiMap = {};
-
- context.append(
- '
-**Note:** In the past, end-to-end testing could be done with a deprecated tool called
-[AngularJS Scenario Runner](http://code.angularjs.org/1.2.16/docs/guide/e2e-testing). That tool
-is now in maintenance mode.
-
-
As applications grow in size and complexity, it becomes unrealistic to rely on manual testing to
verify the correctness of new features, catch bugs and notice regressions. Unit tests
are the first line of defense for catching bugs, but sometimes issues come up with integration
diff --git a/src/ng/browser.js b/src/ng/browser.js
index 3f5f125ed4c3..d5fbc9b6054a 100644
--- a/src/ng/browser.js
+++ b/src/ng/browser.js
@@ -67,7 +67,6 @@ function Browser(window, document, $log, $sniffer) {
/**
* @private
- * Note: this method is used only by scenario runner
* TODO(vojta): prefix this method with $$ ?
* @param {function()} callback Function that will be called when no outstanding request
*/
diff --git a/src/ngMock/browserTrigger.js b/src/ngMock/browserTrigger.js
index 13b2592fea03..07dbbc677e20 100644
--- a/src/ngMock/browserTrigger.js
+++ b/src/ngMock/browserTrigger.js
@@ -2,13 +2,56 @@
(function() {
/**
- * Triggers a browser event. Attempts to choose the right event if one is
- * not specified.
+ * @ngdoc function
+ * @name browserTrigger
+ * @description
+ *
+ * This is a global (window) function that is only available when the {@link ngMock} module is
+ * included.
+ *
+ * It can be used to trigger a native browser event on an element, which is useful for unit testing.
+ *
*
* @param {Object} element Either a wrapped jQuery/jqLite node or a DOMElement
- * @param {string} eventType Optional event type
- * @param {Object=} eventData An optional object which contains additional event data (such as x,y
- * coordinates, keys, etc...) that are passed into the event when triggered
+ * @param {string=} eventType Optional event type. If none is specified, the function tries
+ * to determine the right event type for the element, e.g. `change` for
+ * `input[text]`.
+ * @param {Object=} eventData An optional object which contains additional event data that is used
+ * when creating the event:
+ *
+ * - `bubbles`: [Event.bubbles](https://developer.mozilla.org/docs/Web/API/Event/bubbles).
+ * Not applicable to all events.
+ *
+ * - `cancelable`: [Event.cancelable](https://developer.mozilla.org/docs/Web/API/Event/cancelable).
+ * Not applicable to all events.
+ *
+ * - `charcode`: [charCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/charcode)
+ * for keyboard events (keydown, keypress, and keyup).
+ *
+ * - `elapsedTime`: the elapsedTime for
+ * [TransitionEvent](https://developer.mozilla.org/docs/Web/API/TransitionEvent)
+ * and [AnimationEvent](https://developer.mozilla.org/docs/Web/API/AnimationEvent).
+ *
+ * - `keycode`: [keyCode](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/keycode)
+ * for keyboard events (keydown, keypress, and keyup).
+ *
+ * - `keys`: an array of possible modifier keys (ctrl, alt, shift, meta) for
+ * [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent) and
+ * keyboard events (keydown, keypress, and keyup).
+ *
+ * - `relatedTarget`: the
+ * [relatedTarget](https://developer.mozilla.org/docs/Web/API/MouseEvent/relatedTarget)
+ * for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent).
+ *
+ * - `which`: [which](https://developer.mozilla.org/docs/Web/API/KeyboardEvent/which)
+ * for keyboard events (keydown, keypress, and keyup).
+ *
+ * - `x`: x-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent)
+ * and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent).
+ *
+ * - `y`: y-coordinates for [MouseEvent](https://developer.mozilla.org/docs/Web/API/MouseEvent)
+ * and [TouchEvent](https://developer.mozilla.org/docs/Web/API/TouchEvent).
+ *
*/
window.browserTrigger = function browserTrigger(element, eventType, eventData) {
if (element && !element.nodeName) element = element[0];
From 7df29521d8c1c494f615c49d6c2e1e267e3a6be5 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 3 Nov 2017 17:58:36 +0100
Subject: [PATCH 129/552] refactor($location): remove obsolete workaround for
Firefox bug
The bug was fixed in Firefox 48: https://bugzilla.mozilla.org/show_bug.cgi?id=684208,
and only affected the scenario runner
---
src/ng/location.js | 2 --
src/ngMock/browserTrigger.js | 21 +--------------------
2 files changed, 1 insertion(+), 22 deletions(-)
diff --git a/src/ng/location.js b/src/ng/location.js
index 63cdbb84f0d1..09f08c09cdfe 100644
--- a/src/ng/location.js
+++ b/src/ng/location.js
@@ -938,8 +938,6 @@ function $LocationProvider() {
// update location manually
if ($location.absUrl() !== $browser.url()) {
$rootScope.$apply();
- // hack to work around FF6 bug 684208 when scenario runner clicks on links
- $window.angular['ff-684208-preventDefault'] = true;
}
}
}
diff --git a/src/ngMock/browserTrigger.js b/src/ngMock/browserTrigger.js
index 07dbbc677e20..196772d1e3e9 100644
--- a/src/ngMock/browserTrigger.js
+++ b/src/ngMock/browserTrigger.js
@@ -147,30 +147,11 @@
if (!evnt) return;
- var originalPreventDefault = evnt.preventDefault,
- appWindow = element.ownerDocument.defaultView,
- fakeProcessDefault = true,
- finalProcessDefault,
- angular = appWindow.angular || {};
-
- // igor: temporary fix for https://bugzilla.mozilla.org/show_bug.cgi?id=684208
- angular['ff-684208-preventDefault'] = false;
- evnt.preventDefault = function() {
- fakeProcessDefault = false;
- return originalPreventDefault.apply(evnt, arguments);
- };
-
if (!eventData.bubbles || supportsEventBubblingInDetachedTree() || isAttachedToDocument(element)) {
- element.dispatchEvent(evnt);
+ return element.dispatchEvent(evnt);
} else {
triggerForPath(element, evnt);
}
-
- finalProcessDefault = !(angular['ff-684208-preventDefault'] || !fakeProcessDefault);
-
- delete angular['ff-684208-preventDefault'];
-
- return finalProcessDefault;
};
function supportsTouchEvents() {
From 240a3ddbf12a9bb79754031be95dae4b6bd2dded Mon Sep 17 00:00:00 2001
From: George Kalpakas
Date: Mon, 18 Dec 2017 14:48:15 +0200
Subject: [PATCH 130/552] feat($resource): add support for `request` and
`requestError` interceptors (#15674)
This commit adds `request` and `requestError` interceptors for `$resource`, as
per the documentation found for `$http` interceptors. It is important to note
that returning an error at this stage of the request - before the call to
`$http` - will completely bypass any global interceptors and/or recovery
handlers, as those are added to a separate context. This is intentional;
intercepting a request before it is passed to `$http` indicates that the
resource itself has made a decision, and that it accepts the responsibility for
recovery.
Closes #5146
BREAKING CHANGE:
Previously, calling a `$resource` method would synchronously call
`$http`. Now, it will be called asynchronously (regardless if a
`request`/`requestError` interceptor has been defined.
This is not expected to affect applications at runtime, since the
overall operation is asynchronous already, but may affect assertions in
tests. For example, if you want to assert that `$http` has been called
with specific arguments as a result of a `$resource` call, you now need
to run a `$digest` first, to ensure the (possibly empty) request
interceptor promise has been resolved.
Before:
```js
it('...', function() {
$httpBackend.expectGET('/api/things').respond(...);
var Things = $resource('/api/things');
Things.query();
expect($http).toHaveBeenCalledWith(...);
});
```
After:
```js
it('...', function() {
$httpBackend.expectGET('/api/things').respond(...);
var Things = $resource('/api/things');
Things.query();
$rootScope.$digest();
expect($http).toHaveBeenCalledWith(...);
});
```
---
src/ngResource/resource.js | 23 ++-
test/ngResource/resourceSpec.js | 271 +++++++++++++++++++++++++++++++-
2 files changed, 284 insertions(+), 10 deletions(-)
diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js
index 55760d2f77e9..c8a79274ca2b 100644
--- a/src/ngResource/resource.js
+++ b/src/ngResource/resource.js
@@ -185,11 +185,12 @@ function shallowClearAndCopy(src, dst) {
* for more information.
* - **`responseType`** - `{string}` - see
* [requestType](https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType).
- * - **`interceptor`** - `{Object=}` - The interceptor object has two optional methods -
- * `response` and `responseError`. Both `response` and `responseError` interceptors get called
- * with `http response` object. See {@link ng.$http $http interceptors}. In addition, the
- * resource instance or array object is accessible by the `resource` property of the
- * `http response` object.
+ * - **`interceptor`** - `{Object=}` - The interceptor object has four optional methods -
+ * `request`, `requestError`, `response`, and `responseError`. See
+ * {@link ng.$http $http interceptors} for details. Note that `request`/`requestError`
+ * interceptors are applied before calling `$http`, thus before any global `$http` interceptors.
+ * The resource instance or array object is accessible by the `resource` property of the
+ * `http response` object passed to response interceptors.
* Keep in mind that the associated promise will be resolved with the value returned by the
* response interceptor, if one is specified. The default response interceptor returns
* `response.resource` (i.e. the resource instance or array).
@@ -707,6 +708,9 @@ angular.module('ngResource', ['ng']).
var isInstanceCall = this instanceof Resource;
var value = isInstanceCall ? data : (action.isArray ? [] : new Resource(data));
var httpConfig = {};
+ var requestInterceptor = action.interceptor && action.interceptor.request || undefined;
+ var requestErrorInterceptor = action.interceptor && action.interceptor.requestError ||
+ undefined;
var responseInterceptor = action.interceptor && action.interceptor.response ||
defaultResponseInterceptor;
var responseErrorInterceptor = action.interceptor && action.interceptor.responseError ||
@@ -743,7 +747,14 @@ angular.module('ngResource', ['ng']).
extend({}, extractParams(data, action.params || {}), params),
action.url);
- var promise = $http(httpConfig).then(function(response) {
+ // Start the promise chain
+ var promise = $q.
+ resolve(httpConfig).
+ then(requestInterceptor).
+ catch(requestErrorInterceptor).
+ then($http);
+
+ promise = promise.then(function(response) {
var data = response.data;
if (data) {
diff --git a/test/ngResource/resourceSpec.js b/test/ngResource/resourceSpec.js
index c472ad63f9f4..00fce4b662a8 100644
--- a/test/ngResource/resourceSpec.js
+++ b/test/ngResource/resourceSpec.js
@@ -3,7 +3,7 @@
describe('resource', function() {
describe('basic usage', function() {
- var $resource, CreditCard, callback, $httpBackend, resourceProvider;
+ var $resource, CreditCard, callback, $httpBackend, resourceProvider, $q;
beforeEach(module('ngResource'));
@@ -14,6 +14,7 @@ describe('basic usage', function() {
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
$resource = $injector.get('$resource');
+ $q = $injector.get('$q');
CreditCard = $resource('/CreditCard/:id:verb', {id:'@id.key'}, {
charge:{
method:'post',
@@ -1129,6 +1130,188 @@ describe('basic usage', function() {
});
+ describe('requestInterceptor', function() {
+ var rejectReason = {'lol':'cat'};
+ var successSpy, failureSpy;
+
+ beforeEach(function() {
+ successSpy = jasmine.createSpy('successSpy');
+ failureSpy = jasmine.createSpy('failureSpy');
+ });
+
+ it('should allow per action request interceptor that gets full configuration', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function(httpConfig) {
+ callback(httpConfig);
+ return httpConfig;
+ }
+ }
+ }
+ });
+
+ $httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]);
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+
+ $httpBackend.flush();
+ expect(callback).toHaveBeenCalledOnce();
+ expect(successSpy).toHaveBeenCalledOnce();
+ expect(failureSpy).not.toHaveBeenCalled();
+
+ expect(callback).toHaveBeenCalledWith({
+ 'method': 'get',
+ 'url': '/CreditCard'
+ });
+ });
+
+ it('should call $http with the value returned from requestInterceptor', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function(httpConfig) {
+ httpConfig.url = '/DebitCard';
+ return httpConfig;
+ }
+ }
+ }
+ });
+
+ $httpBackend.expect('GET', '/DebitCard').respond([{id: 1}]);
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+
+ $httpBackend.flush();
+ expect(successSpy).toHaveBeenCalledOnceWith(jasmine.arrayContaining([
+ jasmine.objectContaining({id: 1})
+ ]));
+ expect(failureSpy).not.toHaveBeenCalled();
+ });
+
+ it('should abort the operation if the requestInterceptor rejects the operation', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function() {
+ return $q.reject(rejectReason);
+ }
+ }
+ }
+ });
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+
+ // Make sure all promises resolve.
+ $rootScope.$apply();
+
+ // Ensure the resource promise was rejected
+ expect(resource.$resolved).toBeTruthy();
+ expect(successSpy).not.toHaveBeenCalled();
+ expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason);
+
+ // Ensure that no requests were made.
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should call requestErrorInterceptor if requestInterceptor rejects the operation', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function() {
+ return $q.reject(rejectReason);
+ },
+ requestError: function(rejection) {
+ callback(rejection);
+ return $q.reject(rejection);
+ }
+ }
+ }
+ });
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+ $rootScope.$digest();
+
+ expect(callback).toHaveBeenCalledOnceWith(rejectReason);
+ expect(successSpy).not.toHaveBeenCalled();
+ expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason);
+
+ // Ensure that no requests were made.
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should abort the operation if a requestErrorInterceptor rejects the operation', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function() {
+ return $q.reject(rejectReason);
+ },
+ requestError: function(rejection) {
+ return $q.reject(rejection);
+ }
+ }
+ }
+ });
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+ $rootScope.$apply();
+
+ expect(resource.$resolved).toBeTruthy();
+ expect(successSpy).not.toHaveBeenCalled();
+ expect(failureSpy).toHaveBeenCalledOnceWith(rejectReason);
+
+ // Ensure that no requests were made.
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should continue the operation if a requestErrorInterceptor rescues it', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function(httpConfig) {
+ return $q.reject(httpConfig);
+ },
+ requestError: function(httpConfig) {
+ return $q.resolve(httpConfig);
+ }
+ }
+ }
+ });
+
+ $httpBackend.expect('GET', '/CreditCard').respond([{id: 1}]);
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+ $httpBackend.flush();
+
+ expect(resource.$resolved).toBeTruthy();
+ expect(successSpy).toHaveBeenCalledOnceWith(jasmine.arrayContaining([
+ jasmine.objectContaining({id: 1})
+ ]));
+ expect(failureSpy).not.toHaveBeenCalled();
+
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+ });
+
it('should allow per action response interceptor that gets full response', function() {
CreditCard = $resource('/CreditCard', {}, {
query: {
@@ -1584,6 +1767,7 @@ describe('extra params', function() {
var $http;
var $httpBackend;
var $resource;
+ var $rootScope;
beforeEach(module('ngResource'));
@@ -1593,10 +1777,11 @@ describe('extra params', function() {
});
}));
- beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_) {
+ beforeEach(inject(function(_$http_, _$httpBackend_, _$resource_, _$rootScope_) {
$http = _$http_;
$httpBackend = _$httpBackend_;
$resource = _$resource_;
+ $rootScope = _$rootScope_;
}));
afterEach(function() {
@@ -1610,6 +1795,7 @@ describe('extra params', function() {
var R = $resource('/:foo');
R.get({foo: 'bar', baz: 'qux'});
+ $rootScope.$digest();
expect($http).toHaveBeenCalledWith(jasmine.objectContaining({params: {baz: 'qux'}}));
});
@@ -1624,7 +1810,7 @@ describe('extra params', function() {
});
describe('errors', function() {
- var $httpBackend, $resource, $q;
+ var $httpBackend, $resource, $q, $rootScope;
beforeEach(module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
@@ -1636,6 +1822,7 @@ describe('errors', function() {
$httpBackend = $injector.get('$httpBackend');
$resource = $injector.get('$resource');
$q = $injector.get('$q');
+ $rootScope = $injector.get('$rootScope');
}));
@@ -1838,6 +2025,81 @@ describe('handling rejections', function() {
expect($exceptionHandler.errors[0]).toMatch(/^Error: should be caught/);
}
);
+
+ describe('requestInterceptor', function() {
+ var rejectReason = {'lol':'cat'};
+ var $q, $rootScope;
+ var successSpy, failureSpy, callback;
+
+ beforeEach(inject(function(_$q_, _$rootScope_) {
+ $q = _$q_;
+ $rootScope = _$rootScope_;
+
+ successSpy = jasmine.createSpy('successSpy');
+ failureSpy = jasmine.createSpy('failureSpy');
+ callback = jasmine.createSpy();
+ }));
+
+ it('should call requestErrorInterceptor if requestInterceptor throws an error', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function() {
+ throw rejectReason;
+ },
+ requestError: function(rejection) {
+ callback(rejection);
+ return $q.reject(rejection);
+ }
+ }
+ }
+ });
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+ $rootScope.$apply();
+
+ expect(callback).toHaveBeenCalledOnce();
+ expect(callback).toHaveBeenCalledWith(rejectReason);
+ expect(successSpy).not.toHaveBeenCalled();
+ expect(failureSpy).toHaveBeenCalledOnce();
+ expect(failureSpy).toHaveBeenCalledWith(rejectReason);
+
+ // Ensure that no requests were made.
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+
+ it('should abort the operation if a requestErrorInterceptor throws an exception', function() {
+ var CreditCard = $resource('/CreditCard', {}, {
+ query: {
+ method: 'get',
+ isArray: true,
+ interceptor: {
+ request: function() {
+ return $q.reject();
+ },
+ requestError: function() {
+ throw rejectReason;
+ }
+ }
+ }
+ });
+
+ var resource = CreditCard.query();
+ resource.$promise.then(successSpy, failureSpy);
+ $rootScope.$apply();
+
+ expect(resource.$resolved).toBeTruthy();
+ expect(successSpy).not.toHaveBeenCalled();
+ expect(failureSpy).toHaveBeenCalledOnce();
+ expect(failureSpy).toHaveBeenCalledWith(rejectReason);
+
+ // Ensure that no requests were made.
+ $httpBackend.verifyNoOutstandingRequest();
+ });
+ });
});
describe('cancelling requests', function() {
@@ -1902,7 +2164,7 @@ describe('cancelling requests', function() {
);
it('should use `cancellable` value if passed a non-numeric `timeout` in an action',
- inject(function($log, $q) {
+ inject(function($log, $q, $rootScope) {
spyOn($log, 'debug');
$httpBackend.whenGET('/CreditCard').respond({});
@@ -1915,6 +2177,7 @@ describe('cancelling requests', function() {
});
var creditCard = CreditCard.get();
+ $rootScope.$digest();
expect(creditCard.$cancelRequest).toBeDefined();
expect(httpSpy.calls.argsFor(0)[0].timeout).toEqual(jasmine.any($q));
expect(httpSpy.calls.argsFor(0)[0].timeout.then).toBeDefined();
From 22450e5b7c8486a721db74be32333007273ba584 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Mon, 18 Dec 2017 15:17:56 +0100
Subject: [PATCH 131/552] docs(CHANGELOG.md): add changes for 1.6.8
---
CHANGELOG.md | 22 ++++++++++++++++++++++
1 file changed, 22 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 85809d7573ba..2fb3c1064bea 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,25 @@
+
+# 1.6.8 beneficial-tincture (2017-12-18)
+
+
+## Bug Fixes
+- **$location:**
+ - always decode special chars in `$location.url(value)`
+ ([2bdf71](https://github.com/angular/angular.js/commit/2bdf7126878c87474bb7588ce093d0a3c57b0026))
+ - decode non-component special chars in Hashbang URLS
+ ([57b626](https://github.com/angular/angular.js/commit/57b626a673b7530399d3377dfe770165bec35f8a))
+- **ngModelController:** allow $overrideModelOptions to set updateOn
+ ([55516d](https://github.com/angular/angular.js/commit/55516da2dfc7c5798dce24e9fa930c5ac90c900c),
+ [#16351](https://github.com/angular/angular.js/issues/16351),
+ [#16364](https://github.com/angular/angular.js/issues/16364))
+
+
+## New Features
+- **$parse:** add a hidden interface to retrieve an expression's AST
+ ([f33d95](https://github.com/angular/angular.js/commit/f33d95cfcff6fd0270f92a142df8794cca2013ad),
+ [#16253](https://github.com/angular/angular.js/issues/16253),
+ [#16260](https://github.com/angular/angular.js/issues/16260))
+
# 1.6.7 imperial-backstroke (2017-11-24)
From 62743a54b79187e6c1325c0f6dec0f474147881d Mon Sep 17 00:00:00 2001
From: Georgios Kalpakas
Date: Wed, 7 Sep 2016 23:40:38 +0300
Subject: [PATCH 132/552] feat(currencyFilter): trim whitespace around an empty
currency symbol
In most locales, this won't make a difference (since they do not have
whitespace around their currency symbols). In locales where there is a
whitespace separating the currency symbol from the number, it makes
sense to also remove such whitespace if the user specified an empty
currency symbol (indicating they just want the number).
Fixes #15018
Closes #15085
Closes #15105
---
src/ng/filter/filters.js | 5 ++++-
test/ng/filter/filtersSpec.js | 13 +++++++++++++
2 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/src/ng/filter/filters.js b/src/ng/filter/filters.js
index 5a79a7799929..482b31897c79 100644
--- a/src/ng/filter/filters.js
+++ b/src/ng/filter/filters.js
@@ -68,11 +68,14 @@ function currencyFilter($locale) {
fractionSize = formats.PATTERNS[1].maxFrac;
}
+ // If the currency symbol is empty, trim whitespace around the symbol
+ var currencySymbolRe = !currencySymbol ? /\s*\u00A4\s*/g : /\u00A4/g;
+
// if null or undefined pass it through
return (amount == null)
? amount
: formatNumber(amount, formats.PATTERNS[1], formats.GROUP_SEP, formats.DECIMAL_SEP, fractionSize).
- replace(/\u00A4/g, currencySymbol);
+ replace(currencySymbolRe, currencySymbol);
};
}
diff --git a/test/ng/filter/filtersSpec.js b/test/ng/filter/filtersSpec.js
index 8e3a54a0b2df..0646dfa656af 100644
--- a/test/ng/filter/filtersSpec.js
+++ b/test/ng/filter/filtersSpec.js
@@ -186,6 +186,19 @@ describe('filters', function() {
expect(currency(1.07)).toBe('$1.1');
}));
+
+ it('should trim whitespace around the currency symbol if it is empty',
+ inject(function($locale) {
+ var pattern = $locale.NUMBER_FORMATS.PATTERNS[1];
+ pattern.posPre = pattern.posSuf = ' \u00A4 ';
+ pattern.negPre = pattern.negSuf = ' - \u00A4 - ';
+
+ expect(currency(+1.07, '$')).toBe(' $ 1.07 $ ');
+ expect(currency(-1.07, '$')).toBe(' - $ - 1.07 - $ - ');
+ expect(currency(+1.07, '')).toBe('1.07');
+ expect(currency(-1.07, '')).toBe(' -- 1.07 -- ');
+ })
+ );
});
describe('number', function() {
From 9a521cb3ad223f4f21e7f616138ec9eb5466fcb6 Mon Sep 17 00:00:00 2001
From: Frederik Prijck
Date: Tue, 19 Dec 2017 18:29:53 +0100
Subject: [PATCH 133/552] docs(PULL_REQUEST_TEMPLATE.md): fix broken links in
PR template
Closes #16377
---
.github/PULL_REQUEST_TEMPLATE.md | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index d4c3f81373a3..c10156c9502e 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,4 +1,4 @@
-
+
**What kind of change does this PR introduce? (Bug fix, feature, docs update, ...)**
@@ -16,8 +16,8 @@
**Please check if the PR fulfills these requirements**
-- [ ] The commit message follows our [guidelines](../DEVELOPERS.md#commits)
-- [ ] Fix/Feature: [Docs](../DEVELOPERS.md#documentation) have been added/updated
+- [ ] The commit message follows our [guidelines](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#commits)
+- [ ] Fix/Feature: [Docs](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#documentation) have been added/updated
- [ ] Fix/Feature: Tests have been added; existing tests pass
**Other information**:
From 96dd35afb6156746c8e62e2e1c51feb42931328f Mon Sep 17 00:00:00 2001
From: Georgii Dolzhykov
Date: Fri, 29 Dec 2017 15:30:31 +0200
Subject: [PATCH 134/552] docs(ngModel.NgModelController): correct description
for `$viewChangeListeners`
It was misleading.
Closes #16382
---
src/ng/directive/ngModel.js | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/ng/directive/ngModel.js b/src/ng/directive/ngModel.js
index 7802d05177e6..b9aa13ce13ea 100644
--- a/src/ng/directive/ngModel.js
+++ b/src/ng/directive/ngModel.js
@@ -127,8 +127,10 @@ var ngModelMinErr = minErr('ngModel');
* };
* ```
*
- * @property {Array.} $viewChangeListeners Array of functions to execute whenever the
- * view value has changed. It is called with no arguments, and its return value is ignored.
+ * @property {Array.} $viewChangeListeners Array of functions to execute whenever
+ * a change to {@link ngModel.NgModelController#$viewValue `$viewValue`} has caused a change
+ * to {@link ngModel.NgModelController#$modelValue `$modelValue`}.
+ * It is called with no arguments, and its return value is ignored.
* This can be used in place of additional $watches against the model value.
*
* @property {Object} $error An object hash with all failing validator ids as keys.
From e942e1e988d71b86bade392ef53eeee108e92861 Mon Sep 17 00:00:00 2001
From: Sergey Kryvets
Date: Fri, 29 Dec 2017 10:48:35 -0600
Subject: [PATCH 135/552] docs(developers.md): update node version as specified
in package.json
Closes #16384
---
DEVELOPERS.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index d70897f9ddb9..38a7214516b3 100644
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -18,7 +18,7 @@ machine:
* [Git](http://git-scm.com/): The [Github Guide to
Installing Git][git-setup] is a good source of information.
-* [Node.js v6.x (LTS)](http://nodejs.org): We use Node to generate the documentation, run a
+* [Node.js v8.x (LTS)](http://nodejs.org): We use Node to generate the documentation, run a
development web server, run tests, and generate distributable files. Depending on your system,
you can install Node either from source or as a pre-packaged bundle.
From 88cb9af59487171d1e2728800dcd45f66057517e Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Wed, 3 Jan 2018 18:19:05 +0100
Subject: [PATCH 136/552] docs(DEVELOPERS.md): improve testing section
---
DEVELOPERS.md | 18 ++++++++++++------
1 file changed, 12 insertions(+), 6 deletions(-)
diff --git a/DEVELOPERS.md b/DEVELOPERS.md
index 38a7214516b3..9155a5597f71 100644
--- a/DEVELOPERS.md
+++ b/DEVELOPERS.md
@@ -1,6 +1,7 @@
# Developing AngularJS
* [Development Setup](#setup)
+* [Running Tests](#tests)
* [Coding Rules](#rules)
* [Commit Message Guidelines](#commits)
* [Writing Documentation](#documentation)
@@ -107,6 +108,8 @@ HTTP server. For this purpose, we have made available a local web server based o
http://localhost:8000/build/docs/
```
+## Running Tests
+
### Running the Unit Test Suite
We write unit and integration tests with Jasmine and execute them with Karma. To run all of the
@@ -116,7 +119,7 @@ tests once on Chrome run:
yarn grunt test:unit
```
-To run the tests on other browsers (Chrome, ChromeCanary, Firefox and Safari are pre-configured) use:
+To run the tests on other browsers (Chrome and Firefox are pre-configured) use:
```shell
yarn grunt test:unit --browsers=Chrome,Firefox
@@ -124,16 +127,19 @@ yarn grunt test:unit --browsers=Chrome,Firefox
**Note:** there should be _no spaces between browsers_. `Chrome, Firefox` is INVALID.
+If you want to test locally on Safari, Internet Explorer, or Edge, you must install
+the respective `karma--launcher` from npm.
+
If you have a Saucelabs or Browserstack account, you can also run the unit tests on these services
-via our pre-defined customLaunchers.
+via our pre-defined customLaunchers. See the [karma config file](/karma-shared.conf.js) for all pre-configured browsers.
-For example, to run the whole unit test suite:
+For example, to run the whole unit test suite on selected browsers:
```shell
# Browserstack
-yarn grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10
+yarn grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_10
# Saucelabs
-yarn grunt test:unit --browsers=BS_Chrome,BS_Firefox,BS_Safari,BS_IE_9,BS_IE_10,BS_IE_11,BS_EDGE,BS_iOS_8,BS_iOS_9,BS_iOS_10
+yarn grunt test:unit --browsers=SL_Chrome,SL_Firefox,SL_Safari,SL_IE_9,SL_IE_10,SL_IE_11,SL_EDGE,SL_iOS_10
```
Running these commands requires you to set up [Karma Browserstack][karma-browserstack] or
@@ -483,4 +489,4 @@ You can see an example of a well-defined example [in the `ngRepeat` documentatio
[karma-browserstack]: https://github.com/karma-runner/karma-browserstack-launcher
[karma-saucelabs]: https://github.com/karma-runner/karma-sauce-launcher
[unit-testing]: https://docs.angularjs.org/guide/unit-testing
-[yarn-install]: https://yarnpkg.com/en/docs/install
\ No newline at end of file
+[yarn-install]: https://yarnpkg.com/en/docs/install
From 07d84dd85f44f773766dba6db5ff99e2dd1ad69c Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 5 Jan 2018 15:47:22 +0100
Subject: [PATCH 137/552] chore(*): update copyright year
Closes #16386
---
LICENSE | 2 +-
docs/config/templates/app/indexPage.template.html | 2 +-
src/angular.prefix | 2 +-
src/loader.prefix | 2 +-
src/module.prefix | 2 +-
5 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/LICENSE b/LICENSE
index 4b589a7e7dfa..91f064493681 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
The MIT License
-Copyright (c) 2010-2017 Google, Inc. http://angularjs.org
+Copyright (c) 2010-2018 Google, Inc. http://angularjs.org
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/docs/config/templates/app/indexPage.template.html b/docs/config/templates/app/indexPage.template.html
index 10883c828e02..44ef6ebd7b1a 100644
--- a/docs/config/templates/app/indexPage.template.html
+++ b/docs/config/templates/app/indexPage.template.html
@@ -209,7 +209,7 @@
{{jqueryVersion}}
-
+
```
-## Create the `app` module
+#### Create the `app` module
In the app.js file, create the main application module `app` which depends on the `ngComponentRouter`
module, which is provided by the **Component Router** script.
@@ -547,7 +547,7 @@ must have a base URL.
...
```
-## Bootstrap AngularJS
+#### Bootstrap AngularJS
Bootstrap the AngularJS application and add the top level App Component.
@@ -559,7 +559,7 @@ Bootstrap the AngularJS application and add the top level App Component.
```
-# Implementing the AppComponent
+### Implementing the AppComponent
In the previous section we have created a single top level **App Component**. Let's now create some more
**Routing Components** and wire up **Route Config** for those. We start with a Heroes Feature, which
@@ -577,7 +577,7 @@ We are going to have a `Heroes` Component for the Heroes feature of our applicat
and `HeroDetail` **Components** that will actually display the two different views.
-## App Component
+#### App Component
Configure the **App Component** with a template and **Route Config**:
@@ -598,7 +598,7 @@ Configure the **App Component** with a template and **Route Config**:
The **App Component** has an `` directive in its template. This is where the child **Components**
of this view will be rendered.
-### ngLink
+#### ngLink
We have used the `ng-link` directive to create a link to navigate to the Heroes Component. By using this
directive we don't need to know what the actual URL will be. We can let the Router generate that for us.
@@ -607,7 +607,7 @@ We have included a link to the Crisis Center but have not included the `ng-link`
implemented the CrisisCenter component.
-### Non-terminal Routes
+#### Non-terminal Routes
We need to tell the **Router** that the `Heroes` **Route Definition** is **non-terminal**, that it should
continue to match **Routes** in its child **Components**. We do this by adding a **continuation ellipsis
@@ -616,14 +616,14 @@ Without the **continuation ellipsis** the `HeroList` **Route** will never be mat
stop at the `Heroes` **Routing Component** and not try to match the rest of the URL.
-## Heroes Feature
+### Heroes Feature
Now we can implement our Heroes Feature which consists of three **Components**: `Heroes`, `HeroList` and
`HeroDetail`. The `Heroes` **Routing Component** simply provides a template containing the {@link ngOutlet}
directive and a **Route Config** that defines a set of child **Routes** which delegate through to the
`HeroList` and `HeroDetail` **Components**.
-## HeroesComponent
+### HeroesComponent
Create a new file `heroes.js`, which defines a new AngularJS module for the **Components** of this feature
and registers the Heroes **Component**.
@@ -651,20 +651,20 @@ and also to add the module as a dependency of the `app` module:
angular.module('app', ['ngComponentRouter', 'heroes'])
```
-### Use As Default
+#### Use As Default
The `useAsDefault` property on the `HeroList` **Route Definition**, indicates that if no other **Route
Definition** matches the URL, then this **Route Definition** should be used by default.
-### Route Parameters
+#### Route Parameters
The `HeroDetail` Route has a named parameter (`id`), indicated by prefixing the URL segment with a colon,
as part of its `path` property. The **Router** will match anything in this segment and make that value
available to the HeroDetail **Component**.
-### Terminal Routes
+#### Terminal Routes
Both the Routes in the `HeroesComponent` are terminal, i.e. their routes do not end with `...`. This is
because the `HeroList` and `HeroDetail` will not contain any child routes.
-### Route Names
+#### Route Names
**What is the difference between the `name` and `component` properties on a Route Definition?**
The `component` property in a **Route Definition** defines the **Component** directive that will be rendered
@@ -676,7 +676,7 @@ The `name` property is used to reference the **Route Definition** when generatin
that has the `name` property of `"Heroes"`.
-## HeroList Component
+### HeroList Component
The HeroList **Component** is the first component in the application that actually contains significant
functionality. It loads up a list of heroes from a `heroService` and displays them using `ng-repeat`.
@@ -705,7 +705,7 @@ The template iterates through each `hero` object of the array in the `$ctrl.hero
the `$ctrl` property on the scope of the template.*
-## HeroService
+### HeroService
Our HeroService simulates requesting a list of heroes from a server. In a real application this would be
making an actual server request, perhaps over HTTP.
@@ -735,7 +735,7 @@ Note that both the `getHeroes()` and `getHero(id)` methods return a promise for
in real-life we would have to wait for the server to respond with the data.
-## Router Lifecycle Hooks
+### Router Lifecycle Hooks
**How do I know when my Component is active?**
@@ -780,7 +780,7 @@ By returning a promise for the list of heroes from `$routerOnActivate()` we can
Route until the heroes have arrived successfully. This is similar to how a `resolve` works in {@link ngRoute}.
-## Route Parameters
+### Route Parameters
**How do I access parameters for the current route?**
@@ -811,7 +811,7 @@ by the **Router**. In this code it is used to identify a specific Hero to retrie
This hero is then attached to the **Component** so that it can be accessed in the template.
-## Access to the Current Router
+### Access to the Current Router
**How do I get hold of the current router for my component?**
@@ -882,7 +882,7 @@ Other options for generating this navigation are:
```
this form gives you the possibility of caching the instruction, but is more verbose.
-### Absolute vs Relative Navigation
+#### Absolute vs Relative Navigation
**Why not use `$rootRouter` to do the navigation?**
@@ -894,7 +894,7 @@ to the `HeroListComponent` with the `$rootRouter`, we would have to provide a co
`['App','Heroes','HeroList']`.
-## Extra Parameters
+### Extra Parameters
We can also pass additional optional parameters to routes, which get encoded into the URL and are again
available to the `$routerOnActivate(next, previous)` hook. If we pass the current `id` from the
@@ -936,7 +936,7 @@ Finally, we can use this information to highlight the current hero in the templa
```
-## Crisis Center
+### Crisis Center
Let's implement the Crisis Center feature, which displays a list if crises that need to be dealt with by a hero.
The detailed crisis view has an additional feature where it blocks you from navigating if you have not saved
@@ -951,7 +951,7 @@ changes to the crisis being edited.

-## Crisis Feature
+### Crisis Feature
This feature is very similar to the Heroes feature. It contains the following **Components**:
@@ -962,7 +962,7 @@ This feature is very similar to the Heroes feature. It contains the following **
CrisisService and CrisisListComponent are basically the same as HeroService and HeroListComponent
respectively.
-## Navigation Control Hooks
+### Navigation Control Hooks
**How do I prevent navigation from occurring?**
@@ -979,7 +979,7 @@ can complete, all the **Components** must agree that they can be deactivated or
The **Router** will call the `$routerCanDeactivate` and `$canActivate` hooks, if they are provided. If any
of the hooks resolve to `false` then the navigation is cancelled.
-### Dialog Box Service
+#### Dialog Box Service
We can implement a very simple dialog box that will prompt the user whether they are happy to lose changes they
have made. The result of the prompt is a promise that can be used in a `$routerCanDeactivate` hook.
diff --git a/docs/content/guide/component.ngdoc b/docs/content/guide/component.ngdoc
index f2f99b95a44f..6d378ee51997 100644
--- a/docs/content/guide/component.ngdoc
+++ b/docs/content/guide/component.ngdoc
@@ -445,7 +445,7 @@ angular.module('docsTabsExample', [])
-# Unit-testing Component Controllers
+## Unit-testing Component Controllers
The easiest way to unit-test a component controller is by using the
{@link ngMock.$componentController $componentController} that is included in {@link ngMock}. The
diff --git a/docs/content/guide/di.ngdoc b/docs/content/guide/di.ngdoc
index 58fc09c3bc67..f0ac64c8c290 100644
--- a/docs/content/guide/di.ngdoc
+++ b/docs/content/guide/di.ngdoc
@@ -39,7 +39,7 @@ into `run` blocks.
However, only those that have been **registered beforehand** can be injected. This is different
from services, where the order of registration does not matter.
-See {@link module#module-loading-dependencies Modules} for more details about `run` and `config`
+See {@link module#module-loading Modules} for more details about `run` and `config`
blocks and {@link guide/providers Providers} for more information about the different provider
types.
diff --git a/docs/content/guide/introduction.ngdoc b/docs/content/guide/introduction.ngdoc
index 05704e5302f6..e43dcd30e72b 100644
--- a/docs/content/guide/introduction.ngdoc
+++ b/docs/content/guide/introduction.ngdoc
@@ -64,7 +64,7 @@ Games and GUI editors are examples of applications with intensive and tricky DOM
These kinds of apps are different from CRUD apps, and as a result are probably not a good fit for AngularJS.
In these cases it may be better to use a library with a lower level of abstraction, such as `jQuery`.
-# The Zen of AngularJS
+## The Zen of AngularJS
AngularJS is built around the belief that declarative code is better than imperative when it comes
to building UIs and wiring software components together, while imperative code is excellent for
From 1d804645f7656d592c90216a0355b4948807f6b8 Mon Sep 17 00:00:00 2001
From: Frederik Prijck
Date: Sun, 1 Oct 2017 22:35:21 +0200
Subject: [PATCH 165/552] feat(orderBy): consider `null` and `undefined`
greater than other values
Previously, `null` values where sorted using type `string` resulting in a string
comparison. `undefined` values where compared to other values by type and were
usually considered greater than other values (since their type happens to start
with a `u`), but this was coincidental.
This commit ensures that `null` and `undefined ` values are explicitly
considered greater than other values (with `undefined` > `null`) and will
effectively be put at the end of the sorted list (for ascending order sorting).
Closes #15294
Closes #16376
BREAKING CHANGE:
When using `orderBy` to sort arrays containing `null` values, the `null` values
will be considered "greater than" all other values, except for `undefined`.
Previously, they were sorted as strings. This will result in different (but more
intuitive) sorting order.
Before:
```js
orderByFilter(['a', undefined, 'o', null, 'z']);
//--> 'a', null, 'o', 'z', undefined
```
After:
```js
orderByFilter(['a', undefined, 'o', null, 'z']);
//--> 'a', 'o', 'z', null, undefined
```
---
src/ng/filter/orderBy.js | 25 +++++++++++++++++--------
test/ng/filter/orderBySpec.js | 16 +++++++++++++---
2 files changed, 30 insertions(+), 11 deletions(-)
diff --git a/src/ng/filter/orderBy.js b/src/ng/filter/orderBy.js
index 93e6424d804e..57c374735d5d 100644
--- a/src/ng/filter/orderBy.js
+++ b/src/ng/filter/orderBy.js
@@ -40,6 +40,7 @@
* index: ...
* }
* ```
+ * **Note:** `null` values use `'null'` as their type.
* 2. The comparator function is used to sort the items, based on the derived values, types and
* indices.
*
@@ -74,11 +75,15 @@
*
* The default, built-in comparator should be sufficient for most usecases. In short, it compares
* numbers numerically, strings alphabetically (and case-insensitively), for objects falls back to
- * using their index in the original collection, and sorts values of different types by type.
+ * using their index in the original collection, sorts values of different types by type and puts
+ * `undefined` and `null` values at the end of the sorted list.
*
* More specifically, it follows these steps to determine the relative order of items:
*
- * 1. If the compared values are of different types, compare the types themselves alphabetically.
+ * 1. If the compared values are of different types:
+ * - If one of the values is undefined, consider it "greater than" the other.
+ * - Else if one of the values is null, consider it "greater than" the other.
+ * - Else compare the types themselves alphabetically.
* 2. If both values are of type `string`, compare them alphabetically in a case- and
* locale-insensitive way.
* 3. If both values are objects, compare their indices instead.
@@ -89,9 +94,10 @@
*
* **Note:** If you notice numbers not being sorted as expected, make sure they are actually being
* saved as numbers and not strings.
- * **Note:** For the purpose of sorting, `null` values are treated as the string `'null'` (i.e.
- * `type: 'string'`, `value: 'null'`). This may cause unexpected sort order relative to
- * other values.
+ * **Note:** For the purpose of sorting, `null` and `undefined` are considered "greater than"
+ * any other value (with undefined "greater than" null). This effectively means that `null`
+ * and `undefined` values end up at the end of a list sorted in ascending order.
+ * **Note:** `null` values use `'null'` as their type to be able to distinguish them from objects.
*
* @param {Array|ArrayLike} collection - The collection (array or array-like object) to sort.
* @param {(Function|string|Array.)=} expression - A predicate (or list of
@@ -658,8 +664,7 @@ function orderByFilter($parse) {
function getPredicateValue(value, index) {
var type = typeof value;
if (value === null) {
- type = 'string';
- value = 'null';
+ type = 'null';
} else if (type === 'object') {
value = objectValue(value);
}
@@ -690,7 +695,11 @@ function orderByFilter($parse) {
result = value1 < value2 ? -1 : 1;
}
} else {
- result = type1 < type2 ? -1 : 1;
+ result = (type1 === 'undefined') ? 1 :
+ (type2 === 'undefined') ? -1 :
+ (type1 === 'null') ? 1 :
+ (type2 === 'null') ? -1 :
+ (type1 < type2) ? -1 : 1;
}
return result;
diff --git a/test/ng/filter/orderBySpec.js b/test/ng/filter/orderBySpec.js
index e8f0a4126eff..cab5cb678063 100644
--- a/test/ng/filter/orderBySpec.js
+++ b/test/ng/filter/orderBySpec.js
@@ -309,6 +309,16 @@ describe('Filter: orderBy', function() {
expect(orderBy(items, expr)).toEqual(sorted);
});
+
+ it('should consider null and undefined greater than any other value', function() {
+ var items = [undefined, null, 'z', {}, 999, false];
+ var expr = null;
+ var sorted = [false, 999, {}, 'z', null, undefined];
+ var reversed = [undefined, null, 'z', {}, 999, false];
+
+ expect(orderBy(items, expr)).toEqual(sorted);
+ expect(orderBy(items, expr, true)).toEqual(reversed);
+ });
});
describe('(custom comparator)', function() {
@@ -376,7 +386,7 @@ describe('Filter: orderBy', function() {
});
- it('should treat a value of `null` as `"null"`', function() {
+ it('should treat a value of `null` as type `"null"`', function() {
var items = [null, null];
var expr = null;
var reverse = null;
@@ -386,8 +396,8 @@ describe('Filter: orderBy', function() {
var arg = comparator.calls.argsFor(0)[0];
expect(arg).toEqual(jasmine.objectContaining({
- type: 'string',
- value: 'null'
+ type: 'null',
+ value: null
}));
});
From 8d9984e530873497c39acf7726d51f17d60df909 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 26 Jan 2018 12:02:56 +0100
Subject: [PATCH 166/552] chore(docs-gen): generate list of versions in correct
order
Closes #16419
---
docs/config/processors/versions-data.js | 11 ++++++-----
1 file changed, 6 insertions(+), 5 deletions(-)
diff --git a/docs/config/processors/versions-data.js b/docs/config/processors/versions-data.js
index 22b4570cc326..d52b6c420f69 100644
--- a/docs/config/processors/versions-data.js
+++ b/docs/config/processors/versions-data.js
@@ -55,9 +55,6 @@ module.exports = function generateVersionDocProcessor(gitData) {
if (missesCurrentVersion) versions.push(currentVersion.version);
- // Get the stable release with the highest version
- var highestStableRelease = versions.reverse().find(semverIsStable);
-
versions = versions
.filter(function(versionStr) {
return blacklist.indexOf(versionStr) === -1;
@@ -85,6 +82,9 @@ module.exports = function generateVersionDocProcessor(gitData) {
var latest = sortObject(latestMap, reverse(semver.compare))
.map(function(version) { return makeOption(version, 'Latest'); });
+ // Get the stable release with the highest version
+ var highestStableRelease = versions.find(semverIsStable);
+
// Generate master and stable snapshots
var snapshots = [
makeOption(
@@ -130,14 +130,15 @@ module.exports = function generateVersionDocProcessor(gitData) {
return Object.keys(obj).map(function(key) { return obj[key]; }).sort(cmp);
}
+ // Adapted from
// https://github.com/kaelzhang/node-semver-stable/blob/34dd29842409295d49889d45871bec55a992b7f6/index.js#L25
function semverIsStable(version) {
- var semverObj = semver.parse(version);
+ var semverObj = version.version;
return semverObj === null ? false : !semverObj.prerelease.length;
}
function createSnapshotStableLabel(version) {
- var label = 'v' + version.replace(/.$/, 'x') + '-snapshot';
+ var label = version.label.replace(/.$/, 'x') + '-snapshot';
return label;
}
From a8830d2be402764225dd1108b992965a5f8b1f4d Mon Sep 17 00:00:00 2001
From: Dmitriy
Date: Sun, 28 Jan 2018 14:24:00 +0300
Subject: [PATCH 167/552] feat(input): add `drop` event support (#16420)
---
src/ng/directive/input.js | 4 ++--
test/ng/directive/inputSpec.js | 14 +++++++++++++-
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/src/ng/directive/input.js b/src/ng/directive/input.js
index 228f5fb2366a..7d1bec7cfe9d 100644
--- a/src/ng/directive/input.js
+++ b/src/ng/directive/input.js
@@ -1306,9 +1306,9 @@ function baseInputType(scope, element, attr, ctrl, $sniffer, $browser) {
deferListener(event, this, this.value);
});
- // if user modifies input value using context menu in IE, we need "paste" and "cut" events to catch it
+ // if user modifies input value using context menu in IE, we need "paste", "cut" and "drop" events to catch it
if ($sniffer.hasEvent('paste')) {
- element.on('paste cut', deferListener);
+ element.on('paste cut drop', deferListener);
}
}
diff --git a/test/ng/directive/inputSpec.js b/test/ng/directive/inputSpec.js
index 9c58807345d3..93d2184f969d 100644
--- a/test/ng/directive/inputSpec.js
+++ b/test/ng/directive/inputSpec.js
@@ -439,7 +439,7 @@ describe('input', function() {
}
});
- describe('"keydown", "paste" and "cut" events', function() {
+ describe('"keydown", "paste", "cut" and "drop" events', function() {
beforeEach(function() {
// Force browser to report a lack of an 'input' event
$sniffer.hasEvent = function(eventName) {
@@ -461,6 +461,18 @@ describe('input', function() {
expect($rootScope.name).toEqual('mark');
});
+ it('should update the model on "drop" event if the input value changes', function() {
+ var inputElm = helper.compileInput('');
+
+ browserTrigger(inputElm, 'keydown');
+ $browser.defer.flush();
+ expect(inputElm).toBePristine();
+
+ inputElm.val('mark');
+ browserTrigger(inputElm, 'drop');
+ $browser.defer.flush();
+ expect($rootScope.name).toEqual('mark');
+ });
it('should update the model on "cut" event', function() {
var inputElm = helper.compileInput('');
From 1e9eadcd72dbbd5c67dae8328a63e535cfa91ff9 Mon Sep 17 00:00:00 2001
From: Pete Bacon Darwin
Date: Tue, 30 Jan 2018 14:07:27 +0000
Subject: [PATCH 168/552] feat($sce): handle URL sanitization through the
`$sce` service
Thanks to @rjamet for the original work on this feature.
This is a large patch to handle URLs with the $sce service, similarly to HTML context.
Where we previously sanitized URL attributes when setting attribute value inside the
`$compile` service, we now only apply an `$sce` context requirement and leave the
`$interpolate` service to deal with sanitization.
This commit introduces a new `$sce` context called `MEDIA_URL`, which represents
a URL used as a source for a media element that is not expected to execute code, such as
image, video, audio, etc.
The context hierarchy is setup so that a value trusted as `URL` is also trusted in the
`MEDIA_URL` context, in the same way that the a value trusted as `RESOURCE_URL` is also
trusted in the `URL` context (and transitively also the `MEDIA_URL` context).
The `$sce` service will now automatically attempt to sanitize non-trusted values that
require the `URL` or `MEDIA_URL` context:
* When calling `getTrustedMediaUrl()` a value that is not already a trusted `MEDIA_URL`
will be sanitized using the `imgSrcSanitizationWhitelist`.
* When calling `getTrustedUrl()` a value that is not already a trusted `URL` will be
sanitized using the `aHrefSanitizationWhitelist`.
This results in behaviour that closely matches the previous sanitization behaviour.
To keep rough compatibility with existing apps, we need to allow concatenation of values
that may contain trusted contexts. The following approach is taken for situations that
require a `URL` or `MEDIA_URL` secure context:
* A single trusted value is trusted, e.g. `"{{trustedUrl}}"` and will not be sanitized.
* A single non-trusted value, e.g. `"{{ 'javascript:foo' }}"`, will be handled by
`getTrustedMediaUrl` or `getTrustedUrl)` and sanitized.
* Any concatenation of values (which may or may not be trusted) results in a
non-trusted type that will be handled by `getTrustedMediaUrl` or `getTrustedUrl` once the
concatenation is complete.
E.g. `"javascript:{{safeType}}"` is a concatenation of a non-trusted and a trusted value,
which will be sanitized as a whole after unwrapping the `safeType` value.
* An interpolation containing no expressions will still be handled by `getTrustedMediaUrl` or
`getTrustedUrl`, whereas before this would have been short-circuited in the `$interpolate`
service. E.g. `"some/hard/coded/url"`. This ensures that `ngHref` and similar directives
still securely, even if the URL is hard-coded into a template or index.html (perhaps by
server-side rendering).
BREAKING CHANGES:
If you use `attrs.$set` for URL attributes (a[href] and img[src]) there will no
longer be any automated sanitization of the value. This is in line with other
programmatic operations, such as writing to the innerHTML of an element.
If you are programmatically writing URL values to attributes from untrusted
input then you must sanitize it yourself. You could write your own sanitizer or copy
the private `$$sanitizeUri` service.
Note that values that have been passed through the `$interpolate` service within the
`URL` or `MEDIA_URL` will have already been sanitized, so you would not need to sanitize
these values again.
---
docs/content/error/$compile/srcset.ngdoc | 12 +
src/ng/compile.js | 48 ++-
src/ng/directive/attrs.js | 2 +-
src/ng/interpolate.js | 75 ++--
src/ng/sanitizeUri.js | 38 +-
src/ng/sce.js | 97 +++--
src/ngSanitize/sanitize.js | 5 +-
test/ng/compileSpec.js | 440 +++++++++++++++--------
test/ng/directive/booleanAttrsSpec.js | 208 -----------
test/ng/directive/ngHrefSpec.js | 105 ++++++
test/ng/directive/ngSrcSpec.js | 94 ++++-
test/ng/directive/ngSrcsetSpec.js | 15 +-
test/ng/interpolateSpec.js | 70 ++--
test/ng/sceSpecs.js | 81 ++++-
test/ngSanitize/sanitizeSpec.js | 4 +-
15 files changed, 824 insertions(+), 470 deletions(-)
create mode 100644 docs/content/error/$compile/srcset.ngdoc
create mode 100644 test/ng/directive/ngHrefSpec.js
diff --git a/docs/content/error/$compile/srcset.ngdoc b/docs/content/error/$compile/srcset.ngdoc
new file mode 100644
index 000000000000..cab3de5f4d79
--- /dev/null
+++ b/docs/content/error/$compile/srcset.ngdoc
@@ -0,0 +1,12 @@
+@ngdoc error
+@name $compile:srcset
+@fullName Invalid value passed to `attr.$set('srcset', value)`
+@description
+
+This error occurs if you try to programmatically set the `srcset` attribute with a non-string value.
+
+This can be the case if you tried to avoid the automatic sanitization of the `srcset` value by
+passing a "trusted" value provided by calls to `$sce.trustAsMediaUrl(value)`.
+
+If you want to programmatically set explicitly trusted unsafe URLs, you should use `$sce.trustAsHtml`
+on the whole `img` tag and inject it into the DOM using the `ng-bind-html` directive.
diff --git a/src/ng/compile.js b/src/ng/compile.js
index 4ec3ea5d6d94..6ae2722a6fde 100644
--- a/src/ng/compile.js
+++ b/src/ng/compile.js
@@ -1528,9 +1528,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
- '$controller', '$rootScope', '$sce', '$animate', '$$sanitizeUri',
+ '$controller', '$rootScope', '$sce', '$animate',
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
- $controller, $rootScope, $sce, $animate, $$sanitizeUri) {
+ $controller, $rootScope, $sce, $animate) {
var SIMPLE_ATTR_NAME = /^\w/;
var specialAttrHolder = window.document.createElement('div');
@@ -1679,8 +1679,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
*/
$set: function(key, value, writeAttr, attrName) {
// TODO: decide whether or not to throw an error if "class"
- //is set through this function since it may cause $updateClass to
- //become unstable.
+ // is set through this function since it may cause $updateClass to
+ // become unstable.
var node = this.$$element[0],
booleanKey = getBooleanAttrName(node, key),
@@ -1710,13 +1710,20 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
nodeName = nodeName_(this.$$element);
- if ((nodeName === 'a' && (key === 'href' || key === 'xlinkHref')) ||
- (nodeName === 'img' && key === 'src') ||
- (nodeName === 'image' && key === 'xlinkHref')) {
- // sanitize a[href] and img[src] values
- this[key] = value = $$sanitizeUri(value, nodeName === 'img' || nodeName === 'image');
- } else if (nodeName === 'img' && key === 'srcset' && isDefined(value)) {
- // sanitize img[srcset] values
+ // Sanitize img[srcset] values.
+ if (nodeName === 'img' && key === 'srcset' && value) {
+ if (!isString(value)) {
+ throw $compileMinErr('srcset', 'Can\'t pass trusted values to `$set(\'srcset\', value)`: "{0}"', value.toString());
+ }
+
+ // Such values are a bit too complex to handle automatically inside $sce.
+ // Instead, we sanitize each of the URIs individually, which works, even dynamically.
+
+ // It's not possible to work around this using `$sce.trustAsMediaUrl`.
+ // If you want to programmatically set explicitly trusted unsafe URLs, you should use
+ // `$sce.trustAsHtml` on the whole `img` tag and inject it into the DOM using the
+ // `ng-bind-html` directive.
+
var result = '';
// first check if there are spaces because it's not the same pattern
@@ -1733,16 +1740,16 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
for (var i = 0; i < nbrUrisWith2parts; i++) {
var innerIdx = i * 2;
// sanitize the uri
- result += $$sanitizeUri(trim(rawUris[innerIdx]), true);
+ result += $sce.getTrustedMediaUrl(trim(rawUris[innerIdx]));
// add the descriptor
- result += (' ' + trim(rawUris[innerIdx + 1]));
+ result += ' ' + trim(rawUris[innerIdx + 1]);
}
// split the last item into uri and descriptor
var lastTuple = trim(rawUris[i * 2]).split(/\s/);
// sanitize the last uri
- result += $$sanitizeUri(trim(lastTuple[0]), true);
+ result += $sce.getTrustedMediaUrl(trim(lastTuple[0]));
// and add the last descriptor if any
if (lastTuple.length === 2) {
@@ -3268,14 +3275,18 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
}
var tag = nodeName_(node);
// All tags with src attributes require a RESOURCE_URL value, except for
- // img and various html5 media tags.
+ // img and various html5 media tags, which require the MEDIA_URL context.
if (attrNormalizedName === 'src' || attrNormalizedName === 'ngSrc') {
if (['img', 'video', 'audio', 'source', 'track'].indexOf(tag) === -1) {
return $sce.RESOURCE_URL;
}
+ return $sce.MEDIA_URL;
+ } else if (attrNormalizedName === 'xlinkHref') {
+ // Some xlink:href are okay, most aren't
+ if (tag === 'image') return $sce.MEDIA_URL;
+ if (tag === 'a') return $sce.URL;
+ return $sce.RESOURCE_URL;
} else if (
- // Some xlink:href are okay, most aren't
- (attrNormalizedName === 'xlinkHref' && (tag !== 'image' && tag !== 'a')) ||
// Formaction
(tag === 'form' && attrNormalizedName === 'action') ||
// If relative URLs can go where they are not expected to, then
@@ -3285,6 +3296,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
(tag === 'link' && attrNormalizedName === 'href')
) {
return $sce.RESOURCE_URL;
+ } else if (tag === 'a' && (attrNormalizedName === 'href' ||
+ attrNormalizedName === 'ngHref')) {
+ return $sce.URL;
}
}
diff --git a/src/ng/directive/attrs.js b/src/ng/directive/attrs.js
index af0bf14efd1f..1b646ff5d4c3 100644
--- a/src/ng/directive/attrs.js
+++ b/src/ng/directive/attrs.js
@@ -436,7 +436,7 @@ forEach(['src', 'srcset', 'href'], function(attrName) {
// On IE, if "ng:src" directive declaration is used and "src" attribute doesn't exist
// then calling element.setAttribute('src', 'foo') doesn't do anything, so we need
// to set the property as well to achieve the desired effect.
- // We use attr[attrName] value since $set can sanitize the url.
+ // We use attr[attrName] value since $set might have sanitized the url.
if (msie && propName) element.prop(propName, attr[name]);
});
}
diff --git a/src/ng/interpolate.js b/src/ng/interpolate.js
index 30ad9e3a9ad8..77b863ddcba9 100644
--- a/src/ng/interpolate.js
+++ b/src/ng/interpolate.js
@@ -238,16 +238,21 @@ function $InterpolateProvider() {
* - `context`: evaluation context for all expressions embedded in the interpolated text
*/
function $interpolate(text, mustHaveExpression, trustedContext, allOrNothing) {
+ var contextAllowsConcatenation = trustedContext === $sce.URL || trustedContext === $sce.MEDIA_URL;
+
// Provide a quick exit and simplified result function for text with no interpolation
if (!text.length || text.indexOf(startSymbol) === -1) {
- var constantInterp;
- if (!mustHaveExpression) {
- var unescapedText = unescapeText(text);
- constantInterp = valueFn(unescapedText);
- constantInterp.exp = text;
- constantInterp.expressions = [];
- constantInterp.$$watchDelegate = constantWatchDelegate;
+ if (mustHaveExpression && !contextAllowsConcatenation) return;
+
+ var unescapedText = unescapeText(text);
+ if (contextAllowsConcatenation) {
+ unescapedText = $sce.getTrusted(trustedContext, unescapedText);
}
+ var constantInterp = valueFn(unescapedText);
+ constantInterp.exp = text;
+ constantInterp.expressions = [];
+ constantInterp.$$watchDelegate = constantWatchDelegate;
+
return constantInterp;
}
@@ -256,11 +261,13 @@ function $InterpolateProvider() {
endIndex,
index = 0,
expressions = [],
- parseFns = [],
+ parseFns,
textLength = text.length,
exp,
concat = [],
- expressionPositions = [];
+ expressionPositions = [],
+ singleExpression;
+
while (index < textLength) {
if (((startIndex = text.indexOf(startSymbol, index)) !== -1) &&
@@ -270,10 +277,9 @@ function $InterpolateProvider() {
}
exp = text.substring(startIndex + startSymbolLength, endIndex);
expressions.push(exp);
- parseFns.push($parse(exp, parseStringifyInterceptor));
index = endIndex + endSymbolLength;
expressionPositions.push(concat.length);
- concat.push('');
+ concat.push(''); // Placeholder that will get replaced with the evaluated expression.
} else {
// we did not find an interpolation, so we have to add the remainder to the separators array
if (index !== textLength) {
@@ -283,15 +289,25 @@ function $InterpolateProvider() {
}
}
+ singleExpression = concat.length === 1 && expressionPositions.length === 1;
+ // Intercept expression if we need to stringify concatenated inputs, which may be SCE trusted
+ // objects rather than simple strings
+ // (we don't modify the expression if the input consists of only a single trusted input)
+ var interceptor = contextAllowsConcatenation && singleExpression ? undefined : parseStringifyInterceptor;
+ parseFns = expressions.map(function(exp) { return $parse(exp, interceptor); });
+
// Concatenating expressions makes it hard to reason about whether some combination of
// concatenated values are unsafe to use and could easily lead to XSS. By requiring that a
- // single expression be used for iframe[src], object[src], etc., we ensure that the value
- // that's used is assigned or constructed by some JS code somewhere that is more testable or
- // make it obvious that you bound the value to some user controlled value. This helps reduce
- // the load when auditing for XSS issues.
- if (trustedContext && concat.length > 1) {
- $interpolateMinErr.throwNoconcat(text);
- }
+ // single expression be used for some $sce-managed secure contexts (RESOURCE_URLs mostly),
+ // we ensure that the value that's used is assigned or constructed by some JS code somewhere
+ // that is more testable or make it obvious that you bound the value to some user controlled
+ // value. This helps reduce the load when auditing for XSS issues.
+
+ // Note that URL and MEDIA_URL $sce contexts do not need this, since `$sce` can sanitize the values
+ // passed to it. In that case, `$sce.getTrusted` will be called on either the single expression
+ // or on the overall concatenated string (losing trusted types used in the mix, by design).
+ // Both these methods will sanitize plain strings. Also, HTML could be included, but since it's
+ // only used in srcdoc attributes, this would not be very useful.
if (!mustHaveExpression || expressions.length) {
var compute = function(values) {
@@ -299,13 +315,16 @@ function $InterpolateProvider() {
if (allOrNothing && isUndefined(values[i])) return;
concat[expressionPositions[i]] = values[i];
}
- return concat.join('');
- };
- var getValue = function(value) {
- return trustedContext ?
- $sce.getTrusted(trustedContext, value) :
- $sce.valueOf(value);
+ if (contextAllowsConcatenation) {
+ // If `singleExpression` then `concat[0]` might be a "trusted" value or `null`, rather than a string
+ return $sce.getTrusted(trustedContext, singleExpression ? concat[0] : concat.join(''));
+ } else if (trustedContext && concat.length > 1) {
+ // This context does not allow more than one part, e.g. expr + string or exp + exp.
+ $interpolateMinErr.throwNoconcat(text);
+ }
+ // In an unprivileged context or only one part: just concatenate and return.
+ return concat.join('');
};
return extend(function interpolationFn(context) {
@@ -340,7 +359,13 @@ function $InterpolateProvider() {
function parseStringifyInterceptor(value) {
try {
- value = getValue(value);
+ // In concatenable contexts, getTrusted comes at the end, to avoid sanitizing individual
+ // parts of a full URL. We don't care about losing the trustedness here.
+ // In non-concatenable contexts, where there is only one expression, this interceptor is
+ // not applied to the expression.
+ value = (trustedContext && !contextAllowsConcatenation) ?
+ $sce.getTrusted(trustedContext, value) :
+ $sce.valueOf(value);
return allOrNothing && !isDefined(value) ? value : stringify(value);
} catch (err) {
$exceptionHandler($interpolateMinErr.interr(text, err));
diff --git a/src/ng/sanitizeUri.js b/src/ng/sanitizeUri.js
index f7dc60bf3c41..edda8244e406 100644
--- a/src/ng/sanitizeUri.js
+++ b/src/ng/sanitizeUri.js
@@ -6,6 +6,7 @@
* Private service to sanitize uris for links and images. Used by $compile and $sanitize.
*/
function $$SanitizeUriProvider() {
+
var aHrefSanitizationWhitelist = /^\s*(https?|s?ftp|mailto|tel|file):/,
imgSrcSanitizationWhitelist = /^\s*((https?|ftp|file|blob):|data:image\/)/;
@@ -14,12 +15,16 @@ function $$SanitizeUriProvider() {
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during a[href] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML anchor links.
+ *
+ * Any url due to be assigned to an `a[href]` attribute via interpolation is marked as requiring
+ * the $sce.URL security context. When interpolation occurs a call is made to `$sce.trustAsUrl(url)`
+ * which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize the potentially malicious URL.
+ *
+ * If the URL matches the `aHrefSanitizationWhitelist` regular expression, it is returned unchanged.
*
- * Any url about to be assigned to a[href] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `aHrefSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
@@ -39,12 +44,17 @@ function $$SanitizeUriProvider() {
* Retrieves or overrides the default regular expression that is used for whitelisting of safe
* urls during img[src] sanitization.
*
- * The sanitization is a security measure aimed at prevent XSS attacks via html links.
+ * The sanitization is a security measure aimed at prevent XSS attacks via HTML image src links.
+ *
+ * Any URL due to be assigned to an `img[src]` attribute via interpolation is marked as requiring
+ * the $sce.MEDIA_URL security context. When interpolation occurs a call is made to
+ * `$sce.trustAsMediaUrl(url)` which in turn may call `$$sanitizeUri(url, isMedia)` to sanitize
+ * the potentially malicious URL.
+ *
+ * If the URL matches the `aImgSanitizationWhitelist` regular expression, it is returned unchanged.
*
- * Any url about to be assigned to img[src] via data-binding is first normalized and turned into
- * an absolute url. Afterwards, the url is matched against the `imgSrcSanitizationWhitelist`
- * regular expression. If a match is found, the original url is written into the dom. Otherwise,
- * the absolute url is prefixed with `'unsafe:'` string and only then is it written into the DOM.
+ * If there is no match the URL is returned prefixed with `'unsafe:'` to ensure that when it is written
+ * to the DOM it is inactive and potentially malicious code will not be executed.
*
* @param {RegExp=} regexp New regexp to whitelist urls with.
* @returns {RegExp|ng.$compileProvider} Current RegExp if called without value or self for
@@ -59,10 +69,10 @@ function $$SanitizeUriProvider() {
};
this.$get = function() {
- return function sanitizeUri(uri, isImage) {
- var regex = isImage ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
- var normalizedVal;
- normalizedVal = urlResolve(uri && uri.trim()).href;
+ return function sanitizeUri(uri, isMediaUrl) {
+ // if (!uri) return uri;
+ var regex = isMediaUrl ? imgSrcSanitizationWhitelist : aHrefSanitizationWhitelist;
+ var normalizedVal = urlResolve(uri && uri.trim()).href;
if (normalizedVal !== '' && !normalizedVal.match(regex)) {
return 'unsafe:' + normalizedVal;
}
diff --git a/src/ng/sce.js b/src/ng/sce.js
index 4dc0279fb61e..a5f618ef8fe4 100644
--- a/src/ng/sce.js
+++ b/src/ng/sce.js
@@ -22,12 +22,17 @@ var SCE_CONTEXTS = {
// Style statements or stylesheets. Currently unused in AngularJS.
CSS: 'css',
- // An URL used in a context where it does not refer to a resource that loads code. Currently
- // unused in AngularJS.
+ // An URL used in a context where it refers to the source of media, which are not expected to be run
+ // as scripts, such as an image, audio, video, etc.
+ MEDIA_URL: 'mediaUrl',
+
+ // An URL used in a context where it does not refer to a resource that loads code.
+ // A value that can be trusted as a URL can also trusted as a MEDIA_URL.
URL: 'url',
// RESOURCE_URL is a subtype of URL used where the referred-to resource could be interpreted as
// code. (e.g. ng-include, script src binding, templateUrl)
+ // A value that can be trusted as a RESOURCE_URL, can also trusted as a URL and a MEDIA_URL.
RESOURCE_URL: 'resourceUrl',
// Script. Currently unused in AngularJS.
@@ -242,7 +247,7 @@ function $SceDelegateProvider() {
return resourceUrlBlacklist;
};
- this.$get = ['$injector', function($injector) {
+ this.$get = ['$injector', '$$sanitizeUri', function($injector, $$sanitizeUri) {
var htmlSanitizer = function htmlSanitizer(html) {
throw $sceMinErr('unsafe', 'Attempting to use an unsafe value in a safe context.');
@@ -307,7 +312,8 @@ function $SceDelegateProvider() {
byType[SCE_CONTEXTS.HTML] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.CSS] = generateHolderType(trustedValueHolderBase);
- byType[SCE_CONTEXTS.URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.MEDIA_URL] = generateHolderType(trustedValueHolderBase);
+ byType[SCE_CONTEXTS.URL] = generateHolderType(byType[SCE_CONTEXTS.MEDIA_URL]);
byType[SCE_CONTEXTS.JS] = generateHolderType(trustedValueHolderBase);
byType[SCE_CONTEXTS.RESOURCE_URL] = generateHolderType(byType[SCE_CONTEXTS.URL]);
@@ -386,15 +392,27 @@ function $SceDelegateProvider() {
* @name $sceDelegate#getTrusted
*
* @description
- * Takes any input, and either returns a value that's safe to use in the specified context, or
- * throws an exception.
+ * Given an object and a security context in which to assign it, returns a value that's safe to
+ * use in this context, which was represented by the parameter. To do so, this function either
+ * unwraps the safe type it has been given (for instance, a {@link ng.$sceDelegate#trustAs
+ * `$sceDelegate.trustAs`} result), or it might try to sanitize the value given, depending on
+ * the context and sanitizer availablility.
+ *
+ * The contexts that can be sanitized are $sce.MEDIA_URL, $sce.URL and $sce.HTML. The first two are available
+ * by default, and the third one relies on the `$sanitize` service (which may be loaded through
+ * the `ngSanitize` module). Furthermore, for $sce.RESOURCE_URL context, a plain string may be
+ * accepted if the resource url policy defined by {@link ng.$sceDelegateProvider#resourceUrlWhitelist
+ * `$sceDelegateProvider.resourceUrlWhitelist`} and {@link ng.$sceDelegateProvider#resourceUrlBlacklist
+ * `$sceDelegateProvider.resourceUrlBlacklist`} accepts that resource.
+ *
+ * This function will throw if the safe type isn't appropriate for this context, or if the
+ * value given cannot be accepted in the context (which might be caused by sanitization not
+ * being available, or the value not being recognized as safe).
*
- * In practice, there are several cases. When given a string, this function runs checks
- * and sanitization to make it safe without prior assumptions. When given the result of a {@link
- * ng.$sceDelegate#trustAs `$sceDelegate.trustAs`} call, it returns the originally supplied
- * value if that value's context is valid for this call's context. Finally, this function can
- * also throw when there is no way to turn `maybeTrusted` in a safe value (e.g., no sanitization
- * is available or possible.)
+ *
+ * Disabling auto-escaping is extremely dangerous, it usually creates a Cross Site Scripting
+ * (XSS) vulnerability in your application.
+ *
+
+ `;
return response
.status(200)
From 67f54b660038de2b4346b3e76d66a8dc8ccb1f9b Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Thu, 1 Feb 2018 10:31:32 +0100
Subject: [PATCH 170/552] fix(ngTouch): deprecate the module and its contents
Closes #16427
Closes #16431
---
src/ngTouch/directive/ngSwipe.js | 10 ++++++++++
src/ngTouch/swipe.js | 5 +++++
src/ngTouch/touch.js | 7 +++++++
3 files changed, 22 insertions(+)
diff --git a/src/ngTouch/directive/ngSwipe.js b/src/ngTouch/directive/ngSwipe.js
index e05632044747..5f31fa96470c 100644
--- a/src/ngTouch/directive/ngSwipe.js
+++ b/src/ngTouch/directive/ngSwipe.js
@@ -6,6 +6,11 @@
* @ngdoc directive
* @name ngSwipeLeft
*
+ * @deprecated
+ * sinceVersion="1.7.0"
+ *
+ * See the {@link ngTouch module} documentation for more information.
+ *
* @description
* Specify custom behavior when an element is swiped to the left on a touchscreen device.
* A leftward swipe is a quick, right-to-left slide of the finger.
@@ -42,6 +47,11 @@
* @ngdoc directive
* @name ngSwipeRight
*
+ * @deprecated
+ * sinceVersion="1.7.0"
+ *
+ * See the {@link ngTouch module} documentation for more information.
+ *
* @description
* Specify custom behavior when an element is swiped to the right on a touchscreen device.
* A rightward swipe is a quick, left-to-right slide of the finger.
diff --git a/src/ngTouch/swipe.js b/src/ngTouch/swipe.js
index 013eea3dc6bc..617747f77fab 100644
--- a/src/ngTouch/swipe.js
+++ b/src/ngTouch/swipe.js
@@ -6,6 +6,11 @@
* @ngdoc service
* @name $swipe
*
+ * @deprecated
+ * sinceVersion="1.7.0"
+ *
+ * See the {@link ngTouch module} documentation for more information.
+ *
* @description
* The `$swipe` service is a service that abstracts the messier details of hold-and-drag swipe
* behavior, to make implementing swipe-related directives more convenient.
diff --git a/src/ngTouch/touch.js b/src/ngTouch/touch.js
index 676f6f4a6c9b..d0c2745a876b 100644
--- a/src/ngTouch/touch.js
+++ b/src/ngTouch/touch.js
@@ -11,6 +11,13 @@
*
* See {@link ngTouch.$swipe `$swipe`} for usage.
*
+ * @deprecated
+ * sinceVersion="1.7.0"
+ * The ngTouch module with the {@link ngTouch.$swipe `$swipe`} service and
+ * the {@link ngTouch.ngSwipeLeft} and {@link ngTouch.ngSwipeRight} directives are
+ * deprecated. Instead, stand-alone libraries for touch handling and gesture interaction
+ * should be used, for example [HammerJS](https://hammerjs.github.io/) (which is also used by
+ * Angular).
*/
// define ngTouch module
From e3ece2fad9e1e6d47b5f06815ff186d7e6f44948 Mon Sep 17 00:00:00 2001
From: Georgii Dolzhykov
Date: Thu, 22 Dec 2016 19:17:06 +0300
Subject: [PATCH 171/552] feat(isArray): support Array subclasses in
`angular.isArray()`
Closes #15533
Closes #15541
BREAKING CHANGE:
Previously, `angular.isArray()` was an alias for `Array.isArray()`.
Therefore, objects that prototypally inherit from `Array` where not
considered arrays. Now such objects are considered arrays too.
This change affects several other methods that use `angular.isArray()`
under the hood, such as `angular.copy()`, `angular.equals()`,
`angular.forEach()`, and `angular.merge()`.
This in turn affects how dirty checking treats objects that prototypally
inherit from `Array` (e.g. MobX observable arrays). AngularJS will now
be able to handle these objects better when copying or watching.
---
src/Angular.js | 9 +++++----
test/AngularSpec.js | 31 +++++++++++++++++++++++++++++++
2 files changed, 36 insertions(+), 4 deletions(-)
diff --git a/src/Angular.js b/src/Angular.js
index 7c424897ff18..f5ab043dc8a3 100644
--- a/src/Angular.js
+++ b/src/Angular.js
@@ -219,8 +219,7 @@ function isArrayLike(obj) {
// NodeList objects (with `item` method) and
// other objects with suitable length characteristics are array-like
- return isNumber(length) &&
- (length >= 0 && ((length - 1) in obj || obj instanceof Array) || typeof obj.item === 'function');
+ return isNumber(length) && (length >= 0 && (length - 1) in obj || typeof obj.item === 'function');
}
@@ -635,12 +634,14 @@ function isDate(value) {
* @kind function
*
* @description
- * Determines if a reference is an `Array`. Alias of Array.isArray.
+ * Determines if a reference is an `Array`.
*
* @param {*} value Reference to check.
* @returns {boolean} True if `value` is an `Array`.
*/
-var isArray = Array.isArray;
+function isArray(arr) {
+ return Array.isArray(arr) || arr instanceof Array;
+}
/**
* @description
diff --git a/test/AngularSpec.js b/test/AngularSpec.js
index c10b92e01179..ffe157de589f 100644
--- a/test/AngularSpec.js
+++ b/test/AngularSpec.js
@@ -1254,6 +1254,37 @@ describe('angular', function() {
});
});
+ describe('isArray', function() {
+
+ it('should return true if passed an `Array`', function() {
+ expect(isArray([])).toBe(true);
+ });
+
+ it('should return true if passed an `Array` from a different window context', function() {
+ var iframe = document.createElement('iframe');
+ document.body.appendChild(iframe); // No `contentWindow` if not attached to the DOM.
+ var arr = new iframe.contentWindow.Array();
+ document.body.removeChild(iframe); // Clean up.
+
+ expect(arr instanceof Array).toBe(false);
+ expect(isArray(arr)).toBe(true);
+ });
+
+ it('should return true if passed an object prototypically inherited from `Array`', function() {
+ function FooArray() {}
+ FooArray.prototype = [];
+
+ expect(isArray(new FooArray())).toBe(true);
+ });
+
+ it('should return false if passed non-array objects', function() {
+ expect(isArray(document.body.childNodes)).toBe(false);
+ expect(isArray({length: 0})).toBe(false);
+ expect(isArray({length: 2, 0: 'one', 1: 'two'})).toBe(false);
+ });
+
+ });
+
describe('isArrayLike', function() {
it('should return false if passed a number', function() {
From 16b82c6afe0ab916fef1d6ca78053b00bf5ada83 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 2 Feb 2018 10:02:06 +0100
Subject: [PATCH 172/552] fix($animate): let cancel() reject the runner promise
Closes #14204
Closes #16373
BREAKING CHANGE:
$animate.cancel(runner) now rejects the underlying
promise and calls the catch() handler on the runner
returned by $animate functions (enter, leave, move,
addClass, removeClass, setClass, animate).
Previously it would resolve the promise as if the animation
had ended successfully.
Example:
```js
var runner = $animate.addClass('red');
runner.then(function() { console.log('success')});
runner.catch(function() { console.log('cancelled')});
runner.cancel();
```
Pre-1.7.0, this logs 'success', 1.7.0 and later it logs 'cancelled'.
To migrate, add a catch() handler to your animation runners.
---
src/ng/animate.js | 88 +++++++++++++---
test/ngAnimate/animateSpec.js | 190 ++++++++++++++++++++++++++++++++++
2 files changed, 266 insertions(+), 12 deletions(-)
diff --git a/src/ng/animate.js b/src/ng/animate.js
index 1f9bc9028cf0..60a9bc3d04a9 100644
--- a/src/ng/animate.js
+++ b/src/ng/animate.js
@@ -464,13 +464,77 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* @ngdoc method
* @name $animate#cancel
* @kind function
- * @description Cancels the provided animation.
- *
- * @param {Promise} animationPromise The animation promise that is returned when an animation is started.
+ * @description Cancels the provided animation and applies the end state of the animation.
+ * Note that this does not cancel the underlying operation, e.g. the setting of classes or
+ * adding the element to the DOM.
+ *
+ * @param {animationRunner} animationRunner An animation runner returned by an $animate function.
+ *
+ * @example
+
+
+ angular.module('animationExample', ['ngAnimate']).component('cancelExample', {
+ templateUrl: 'template.html',
+ controller: function($element, $animate) {
+ this.runner = null;
+
+ this.addClass = function() {
+ this.runner = $animate.addClass($element.find('div'), 'red');
+ var ctrl = this;
+ this.runner.finally(function() {
+ ctrl.runner = null;
+ });
+ };
+
+ this.removeClass = function() {
+ this.runner = $animate.removeClass($element.find('div'), 'red');
+ var ctrl = this;
+ this.runner.finally(function() {
+ ctrl.runner = null;
+ });
+ };
+
+ this.cancel = function() {
+ $animate.cancel(this.runner);
+ };
+ }
+ });
+
+
+
+
+
+
+
+
+
CSS-Animated Text
+
+
+
+
+
+
+ .red-add, .red-remove {
+ transition: all 4s cubic-bezier(0.250, 0.460, 0.450, 0.940);
+ }
+
+ .red,
+ .red-add.red-add-active {
+ color: #FF0000;
+ font-size: 40px;
+ }
+
+ .red-remove.red-remove-active {
+ font-size: 10px;
+ color: black;
+ }
+
+
+
*/
cancel: function(runner) {
- if (runner.end) {
- runner.end();
+ if (runner.cancel) {
+ runner.cancel();
}
},
@@ -496,7 +560,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
enter: function(element, parent, after, options) {
parent = parent && jqLite(parent);
@@ -528,7 +592,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
move: function(element, parent, after, options) {
parent = parent && jqLite(parent);
@@ -555,7 +619,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
leave: function(element, options) {
return $$animateQueue.push(element, 'leave', prepareAnimateOptions(options), function() {
@@ -585,7 +649,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} animationRunner the animation runner
*/
addClass: function(element, className, options) {
options = prepareAnimateOptions(options);
@@ -615,7 +679,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
removeClass: function(element, className, options) {
options = prepareAnimateOptions(options);
@@ -646,7 +710,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
setClass: function(element, add, remove, options) {
options = prepareAnimateOptions(options);
@@ -693,7 +757,7 @@ var $AnimateProvider = ['$provide', /** @this */ function($provide) {
* - **removeClass** - `{string}` - space-separated CSS classes to remove from element
* - **to** - `{Object}` - CSS properties & values at end of animation. Must have matching `from`
*
- * @return {Promise} the animation callback promise
+ * @return {Runner} the animation runner
*/
animate: function(element, from, to, className, options) {
options = prepareAnimateOptions(options);
diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js
index e8c0131a8a16..484836ca66e0 100644
--- a/test/ngAnimate/animateSpec.js
+++ b/test/ngAnimate/animateSpec.js
@@ -790,6 +790,7 @@ describe('animations', function() {
expect(element).toHaveClass('red');
}));
+
it('removeClass() should issue a removeClass animation with the correct DOM operation', inject(function($animate, $rootScope) {
parent.append(element);
element.addClass('blue');
@@ -934,6 +935,195 @@ describe('animations', function() {
}));
});
+
+ describe('$animate.cancel()', function() {
+
+ it('should cancel enter()', inject(function($animate, $rootScope) {
+ expect(parent.children().length).toBe(0);
+
+ options.foo = 'bar';
+ var spy = jasmine.createSpy('cancelCatch');
+
+ var runner = $animate.enter(element, parent, null, options);
+
+ runner.catch(spy);
+
+ expect(parent.children().length).toBe(1);
+
+ $rootScope.$digest();
+
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('enter');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
+
+ $animate.cancel(runner);
+ // Since enter() immediately adds the element, we can only check if the
+ // element is still at the position
+ expect(parent.children().length).toBe(1);
+
+ $rootScope.$digest();
+
+ // Catch handler is called after digest
+ expect(spy).toHaveBeenCalled();
+ }));
+
+
+ it('should cancel move()', inject(function($animate, $rootScope) {
+ parent.append(element);
+
+ expect(parent.children().length).toBe(1);
+ expect(parent2.children().length).toBe(0);
+
+ options.foo = 'bar';
+ var spy = jasmine.createSpy('cancelCatch');
+
+ var runner = $animate.move(element, parent2, null, options);
+ runner.catch(spy);
+
+ expect(parent.children().length).toBe(0);
+ expect(parent2.children().length).toBe(1);
+
+ $rootScope.$digest();
+
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('move');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
+
+ $animate.cancel(runner);
+ // Since moves() immediately moves the element, we can only check if the
+ // element is still at the correct position
+ expect(parent.children().length).toBe(0);
+ expect(parent2.children().length).toBe(1);
+
+ $rootScope.$digest();
+
+ // Catch handler is called after digest
+ expect(spy).toHaveBeenCalled();
+ }));
+
+
+ it('cancel leave()', inject(function($animate, $rootScope) {
+ parent.append(element);
+ options.foo = 'bar';
+ var spy = jasmine.createSpy('cancelCatch');
+
+ var runner = $animate.leave(element, options);
+
+ runner.catch(spy);
+ $rootScope.$digest();
+
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('leave');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
+
+ expect(element.parent().length).toBe(1);
+
+ $animate.cancel(runner);
+ // Animation concludes immediately
+ expect(element.parent().length).toBe(0);
+ expect(spy).not.toHaveBeenCalled();
+
+ $rootScope.$digest();
+ // Catch handler is called after digest
+ expect(spy).toHaveBeenCalled();
+ }));
+
+ it('should cancel addClass()', inject(function($animate, $rootScope) {
+ parent.append(element);
+ options.foo = 'bar';
+ var runner = $animate.addClass(element, 'red', options);
+ var spy = jasmine.createSpy('cancelCatch');
+
+ runner.catch(spy);
+ $rootScope.$digest();
+
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('addClass');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
+
+ $animate.cancel(runner);
+ expect(element).toHaveClass('red');
+ expect(spy).not.toHaveBeenCalled();
+
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalled();
+ }));
+
+
+ it('should cancel setClass()', inject(function($animate, $rootScope) {
+ parent.append(element);
+ element.addClass('red');
+ options.foo = 'bar';
+
+ var runner = $animate.setClass(element, 'blue', 'red', options);
+ var spy = jasmine.createSpy('cancelCatch');
+
+ runner.catch(spy);
+ $rootScope.$digest();
+
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('setClass');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
+
+ $animate.cancel(runner);
+ expect(element).toHaveClass('blue');
+ expect(element).not.toHaveClass('red');
+ expect(spy).not.toHaveBeenCalled();
+
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalled();
+ }));
+
+
+ it('should cancel removeClass()', inject(function($animate, $rootScope) {
+ parent.append(element);
+ element.addClass('red blue');
+
+ options.foo = 'bar';
+ var runner = $animate.removeClass(element, 'red', options);
+ var spy = jasmine.createSpy('cancelCatch');
+
+ runner.catch(spy);
+ $rootScope.$digest();
+
+ expect(capturedAnimation[0]).toBe(element);
+ expect(capturedAnimation[1]).toBe('removeClass');
+ expect(capturedAnimation[2].foo).toEqual(options.foo);
+
+ $animate.cancel(runner);
+ expect(element).not.toHaveClass('red');
+ expect(element).toHaveClass('blue');
+
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalled();
+ }));
+
+
+ it('should cancel animate()',
+ inject(function($animate, $rootScope) {
+
+ parent.append(element);
+
+ var fromStyle = { color: 'blue' };
+ var options = { addClass: 'red' };
+
+ var runner = $animate.animate(element, fromStyle, null, null, options);
+ var spy = jasmine.createSpy('cancelCatch');
+
+ runner.catch(spy);
+ $rootScope.$digest();
+
+ expect(capturedAnimation).toBeTruthy();
+
+ $animate.cancel(runner);
+ expect(element).toHaveClass('red');
+
+ $rootScope.$digest();
+ expect(spy).toHaveBeenCalled();
+ }));
+ });
+
+
describe('parent animations', function() {
they('should not cancel a pre-digest parent class-based animation if a child $prop animation is set to run',
['structural', 'class-based'], function(animationType) {
From b969c3e3540d05a781404beebecdd4fa4ceb2d2e Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 2 Feb 2018 11:19:32 +0100
Subject: [PATCH 173/552] docs(changelog): add changes for 1.6.9
---
CHANGELOG.md | 28 ++++++++++++++++++++++++++++
1 file changed, 28 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 2fb3c1064bea..40b8fc4d4a87 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,31 @@
+
+# 1.6.9 fiery-basilisk (2018-02-02)
+
+
+## Bug Fixes
+- **input:** add `drop` event support for IE
+ ([5dc076](https://github.com/angular/angular.js/commit/5dc07667de00c5e85fd69c5b7b7fe4fb5fd65a77))
+- **ngMessages:** prevent memory leak from messages that are never attached
+ ([9d058d](https://github.com/angular/angular.js/commit/9d058de04bb78694b83179e9b97bc40214eca01a),
+ [#16389](https://github.com/angular/angular.js/issues/16389),
+ [#16404](https://github.com/angular/angular.js/issues/16404),
+ [#16406](https://github.com/angular/angular.js/issues/16406))
+- **ngTransclude:** remove terminal: true
+ ([1d826e](https://github.com/angular/angular.js/commit/1d826e2f1e941d14c3c56d7a0249f5796ba11f85),
+ [#16411](https://github.com/angular/angular.js/issues/16411),
+ [#16412](https://github.com/angular/angular.js/issues/16412))
+- **$sanitize:** sanitize `xml:base` attributes
+ ([b9ef65](https://github.com/angular/angular.js/commit/b9ef6585e10477fbbf912a971fe0b390bca692a6))
+
+
+## New Features
+- **currencyFilter:** trim whitespace around an empty currency symbol
+ ([367390](https://github.com/angular/angular.js/commit/3673909896efb6ff47546caf7fc61549f193e043),
+ [#15018](https://github.com/angular/angular.js/issues/15018),
+ [#15085](https://github.com/angular/angular.js/issues/15085),
+ [#15105](https://github.com/angular/angular.js/issues/15105))
+
+
# 1.6.8 beneficial-tincture (2017-12-18)
From d3bffc547697c8f2059f32402c9f02092c1a8b5c Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Fri, 2 Feb 2018 12:31:49 +0100
Subject: [PATCH 174/552] chore(docs.angularjs.org): add robots.txt
---
docs/app/assets/robots.txt | 11 +++++++++++
scripts/docs.angularjs.org-firebase/firebase.json | 2 +-
2 files changed, 12 insertions(+), 1 deletion(-)
create mode 100644 docs/app/assets/robots.txt
diff --git a/docs/app/assets/robots.txt b/docs/app/assets/robots.txt
new file mode 100644
index 000000000000..a0de3bfda6b4
--- /dev/null
+++ b/docs/app/assets/robots.txt
@@ -0,0 +1,11 @@
+User-agent: *
+
+Disallow: /components/
+Disallow: /examples/
+Disallow: /img/
+Disallow: /js/
+Disallow: /partials/
+Disallow: /ptore2e/
+Disallow: /*.js$
+Disallow: /*.map$
+Disallow: /Error404.html
diff --git a/scripts/docs.angularjs.org-firebase/firebase.json b/scripts/docs.angularjs.org-firebase/firebase.json
index 8f82fc2d8e9c..5f5d70dc02d6 100644
--- a/scripts/docs.angularjs.org-firebase/firebase.json
+++ b/scripts/docs.angularjs.org-firebase/firebase.json
@@ -23,7 +23,7 @@
"destination": "/index-production.html"
},
{
- "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.map|.json|.css|.svg|.ttf|.woff|.woff2|.eot)",
+ "source": "**/*!(.jpg|.jpeg|.gif|.png|.html|.js|.map|.json|.css|.svg|.ttf|.txt|.woff|.woff2|.eot)",
"destination": "/index-production.html"
}
]
From fb00991460cf69ae8bc7f1f826363d09c73c0d5e Mon Sep 17 00:00:00 2001
From: frederikprijck
Date: Sun, 4 Feb 2018 10:20:46 +0100
Subject: [PATCH 175/552] fix($templateRequest): always return the template
that is stored in the cache
Previously, `$templateRequest` returned the raw `$http` response data on the
first request for a template and then the value from the cache for subsequent
requests.
If the value is transformed when being added to the cache (by decorating
`$templateCache.put`) the return value of `$templateRequest` would be
inconsistent depending upon when the request is made.
This commit ensures the cached value is returned instead of the raw `$http`
response data, thus allowing the `$templateCache` service to be decorated.
Closes #16225
---
src/ng/templateRequest.js | 3 +--
test/ng/templateRequestSpec.js | 18 ++++++++++++++++++
2 files changed, 19 insertions(+), 2 deletions(-)
diff --git a/src/ng/templateRequest.js b/src/ng/templateRequest.js
index 7b3b04261e56..ff699d6cd0ef 100644
--- a/src/ng/templateRequest.js
+++ b/src/ng/templateRequest.js
@@ -99,8 +99,7 @@ function $TemplateRequestProvider() {
handleRequestFn.totalPendingRequests--;
})
.then(function(response) {
- $templateCache.put(tpl, response.data);
- return response.data;
+ return $templateCache.put(tpl, response.data);
}, handleError);
function handleError(resp) {
diff --git a/test/ng/templateRequestSpec.js b/test/ng/templateRequestSpec.js
index cb9c1c6f6ce8..3ca323613103 100644
--- a/test/ng/templateRequestSpec.js
+++ b/test/ng/templateRequestSpec.js
@@ -114,6 +114,24 @@ describe('$templateRequest', function() {
expect($templateCache.get('tpl.html')).toBe('matias');
}));
+ it('should return the cached value on the first request',
+ inject(function($rootScope, $templateRequest, $templateCache, $httpBackend) {
+
+ $httpBackend.expectGET('tpl.html').respond('matias');
+ spyOn($templateCache, 'put').and.returnValue('_matias');
+
+ var content = [];
+ function tplRequestCb(html) {
+ content.push(html);
+ }
+
+ $templateRequest('tpl.html').then(tplRequestCb);
+ $rootScope.$digest();
+ $httpBackend.flush();
+
+ expect(content[0]).toBe('_matias');
+ }));
+
it('should call `$exceptionHandler` on request error', function() {
module(function($exceptionHandlerProvider) {
$exceptionHandlerProvider.mode('log');
From fbe679dfbcb2108249931d44f452d09da6c98477 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Mon, 5 Feb 2018 12:35:12 +0100
Subject: [PATCH 176/552] chore(doc-gen): generate sitemap.xml
---
docs/config/index.js | 1 +
docs/config/processors/sitemap.js | 25 +++++++++++++++++++
.../config/templates/app/sitemap.template.xml | 7 ++++++
3 files changed, 33 insertions(+)
create mode 100644 docs/config/processors/sitemap.js
create mode 100644 docs/config/templates/app/sitemap.template.xml
diff --git a/docs/config/index.js b/docs/config/index.js
index ab5e45a3f8dc..4ddf7922c7bd 100644
--- a/docs/config/index.js
+++ b/docs/config/index.js
@@ -31,6 +31,7 @@ module.exports = new Package('angularjs', [
.processor(require('./processors/keywords'))
.processor(require('./processors/pages-data'))
.processor(require('./processors/versions-data'))
+.processor(require('./processors/sitemap'))
.config(function(dgeni, log, readFilesProcessor, writeFilesProcessor) {
diff --git a/docs/config/processors/sitemap.js b/docs/config/processors/sitemap.js
new file mode 100644
index 000000000000..aea84da9a17a
--- /dev/null
+++ b/docs/config/processors/sitemap.js
@@ -0,0 +1,25 @@
+'use strict';
+
+var exclusionRegex = /^index|examples\/|ptore2e\//;
+
+module.exports = function createSitemap() {
+ return {
+ $runAfter: ['paths-computed'],
+ $runBefore: ['rendering-docs'],
+ $process: function(docs) {
+ docs.push({
+ id: 'sitemap.xml',
+ path: 'sitemap.xml',
+ outputPath: '../sitemap.xml',
+ template: 'sitemap.template.xml',
+ urls: docs.filter(function(doc) {
+ return doc.path &&
+ doc.outputPath &&
+ !exclusionRegex.test(doc.outputPath);
+ }).map(function(doc) {
+ return doc.path;
+ })
+ });
+ }
+ };
+};
diff --git a/docs/config/templates/app/sitemap.template.xml b/docs/config/templates/app/sitemap.template.xml
new file mode 100644
index 000000000000..56953d903920
--- /dev/null
+++ b/docs/config/templates/app/sitemap.template.xml
@@ -0,0 +1,7 @@
+
+
+ {%- for url in doc.urls %}
+
+ https://docs.angularjs.org/{$ url $}
+ {% endfor %}
+
\ No newline at end of file
From ea04dbb229ec69a7ffc954d57496f058d6ce6dcb Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Mon, 5 Feb 2018 12:38:44 +0100
Subject: [PATCH 177/552] chore(code.angularjs.org): fix robots.txt
- allow all-versions-data.js in snapshot, which is used by docs.angularjs.org
- disallow access to folders like docs-0.9.2 etc which are used by early versions
---
scripts/code.angularjs.org-firebase/public/robots.txt | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/scripts/code.angularjs.org-firebase/public/robots.txt b/scripts/code.angularjs.org-firebase/public/robots.txt
index 480082428fa1..e83f2c10453d 100644
--- a/scripts/code.angularjs.org-firebase/public/robots.txt
+++ b/scripts/code.angularjs.org-firebase/public/robots.txt
@@ -1,5 +1,6 @@
User-agent: *
-Disallow: /*docs/
+Disallow: /*docs*/
Disallow: /*i18n/
Disallow: /*.zip$
+Allow: /snapshot/docs/js/all-versions-data.js
From 7d50b2e9eea9c7e8950beffc1e139c4e05af00f5 Mon Sep 17 00:00:00 2001
From: Martin Staffa
Date: Mon, 5 Feb 2018 13:07:07 +0100
Subject: [PATCH 178/552] chore(docs.angularjs.org): allow robots to access js
and css
Otherwise, the google bot cannot execute the JS
---
docs/app/assets/robots.txt | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/docs/app/assets/robots.txt b/docs/app/assets/robots.txt
index a0de3bfda6b4..c00cb3c20320 100644
--- a/docs/app/assets/robots.txt
+++ b/docs/app/assets/robots.txt
@@ -1,11 +1,9 @@
User-agent: *
-Disallow: /components/
Disallow: /examples/
Disallow: /img/
-Disallow: /js/
Disallow: /partials/
Disallow: /ptore2e/
-Disallow: /*.js$
-Disallow: /*.map$
+Disallow: /*.js$ # The js files in the root are used by the embedded examples, not by the app itself
+Disallow: /*.map$ # The map files in the root are used by the embedded examples, not by the app itself
Disallow: /Error404.html
From 8d6ac5f3178cb6ead6b3b7526c50cd1c07112097 Mon Sep 17 00:00:00 2001
From: Maksim Ryzhikov
Date: Sat, 11 Nov 2017 16:18:17 +0300
Subject: [PATCH 179/552] feat($sanitize): support enhancing
elements/attributes white-lists
Fixes #5900
Closes #16326
---
src/ngSanitize/sanitize.js | 139 +++++++++++++++++++++++++++++---
test/ngSanitize/sanitizeSpec.js | 50 ++++++++++++
2 files changed, 176 insertions(+), 13 deletions(-)
diff --git a/src/ngSanitize/sanitize.js b/src/ngSanitize/sanitize.js
index b08850fba065..48ddad82341c 100644
--- a/src/ngSanitize/sanitize.js
+++ b/src/ngSanitize/sanitize.js
@@ -15,6 +15,7 @@ var $sanitizeMinErr = angular.$$minErr('$sanitize');
var bind;
var extend;
var forEach;
+var isArray;
var isDefined;
var lowercase;
var noop;
@@ -144,9 +145,11 @@ var htmlSanitizeWriter;
* Creates and configures {@link $sanitize} instance.
*/
function $SanitizeProvider() {
+ var hasBeenInstantiated = false;
var svgEnabled = false;
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
+ hasBeenInstantiated = true;
if (svgEnabled) {
extend(validElements, svgElements);
}
@@ -187,7 +190,7 @@ function $SanitizeProvider() {
*
*
* @param {boolean=} flag Enable or disable SVG support in the sanitizer.
- * @returns {boolean|ng.$sanitizeProvider} Returns the currently configured value if called
+ * @returns {boolean|$sanitizeProvider} Returns the currently configured value if called
* without an argument or self for chaining otherwise.
*/
this.enableSvg = function(enableSvg) {
@@ -199,6 +202,105 @@ function $SanitizeProvider() {
}
};
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#addValidElements
+ * @kind function
+ *
+ * @description
+ * Extends the built-in lists of valid HTML/SVG elements, i.e. elements that are considered safe
+ * and are not stripped off during sanitization. You can extend the following lists of elements:
+ *
+ * - `htmlElements`: A list of elements (tag names) to extend the current list of safe HTML
+ * elements. HTML elements considered safe will not be removed during sanitization. All other
+ * elements will be stripped off.
+ *
+ * - `htmlVoidElements`: This is similar to `htmlElements`, but marks the elements as
+ * "void elements" (similar to HTML
+ * [void elements](https://rawgit.com/w3c/html/html5.1-2/single-page.html#void-elements)). These
+ * elements have no end tag and cannot have content.
+ *
+ * - `svgElements`: This is similar to `htmlElements`, but for SVG elements. This list is only
+ * taken into account if SVG is {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for
+ * `$sanitize`.
+ *
+ *
+ * This method must be called during the {@link angular.Module#config config} phase. Once the
+ * `$sanitize` service has been instantiated, this method has no effect.
+ *
+ *
+ *
+ * Keep in mind that extending the built-in lists of elements may expose your app to XSS or
+ * other vulnerabilities. Be very mindful of the elements you add.
+ *
+ *
+ * @param {Array|Object} elements - A list of valid HTML elements or an object with one or
+ * more of the following properties:
+ * - **htmlElements** - `{Array}` - A list of elements to extend the current list of
+ * HTML elements.
+ * - **htmlVoidElements** - `{Array}` - A list of elements to extend the current list of
+ * void HTML elements; i.e. elements that do not have an end tag.
+ * - **svgElements** - `{Array}` - A list of elements to extend the current list of SVG
+ * elements. The list of SVG elements is only taken into account if SVG is
+ * {@link ngSanitize.$sanitizeProvider#enableSvg enabled} for `$sanitize`.
+ *
+ * Passing an array (`[...]`) is equivalent to passing `{htmlElements: [...]}`.
+ *
+ * @return {$sanitizeProvider} Returns self for chaining.
+ */
+ this.addValidElements = function(elements) {
+ if (!hasBeenInstantiated) {
+ if (isArray(elements)) {
+ elements = {htmlElements: elements};
+ }
+
+ addElementsTo(svgElements, elements.svgElements);
+ addElementsTo(voidElements, elements.htmlVoidElements);
+ addElementsTo(validElements, elements.htmlVoidElements);
+ addElementsTo(validElements, elements.htmlElements);
+ }
+
+ return this;
+ };
+
+
+ /**
+ * @ngdoc method
+ * @name $sanitizeProvider#addValidAttrs
+ * @kind function
+ *
+ * @description
+ * Extends the built-in list of valid attributes, i.e. attributes that are considered safe and are
+ * not stripped off during sanitization.
+ *
+ * **Note**:
+ * The new attributes will not be treated as URI attributes, which means their values will not be
+ * sanitized as URIs using `$compileProvider`'s
+ * {@link ng.$compileProvider#aHrefSanitizationWhitelist aHrefSanitizationWhitelist} and
+ * {@link ng.$compileProvider#imgSrcSanitizationWhitelist imgSrcSanitizationWhitelist}.
+ *
+ *
+ * This method must be called during the {@link angular.Module#config config} phase. Once the
+ * `$sanitize` service has been instantiated, this method has no effect.
+ *
+ *
+ *
+ * Keep in mind that extending the built-in list of attributes may expose your app to XSS or
+ * other vulnerabilities. Be very mindful of the attributes you add.
+ *
+ *
+ * @param {Array} attrs - A list of valid attributes.
+ *
+ * @returns {$sanitizeProvider} Returns self for chaining.
+ */
+ this.addValidAttrs = function(attrs) {
+ if (!hasBeenInstantiated) {
+ extend(validAttrs, arrayToMap(attrs, true));
+ }
+ return this;
+ };
+
//////////////////////////////////////////////////////////////////////////////////////////////////
// Private stuff
//////////////////////////////////////////////////////////////////////////////////////////////////
@@ -206,6 +308,7 @@ function $SanitizeProvider() {
bind = angular.bind;
extend = angular.extend;
forEach = angular.forEach;
+ isArray = angular.isArray;
isDefined = angular.isDefined;
lowercase = angular.$$lowercase;
noop = angular.noop;
@@ -230,23 +333,23 @@ function $SanitizeProvider() {
// Safe Void Elements - HTML5
// http://dev.w3.org/html5/spec/Overview.html#void-elements
- var voidElements = toMap('area,br,col,hr,img,wbr');
+ var voidElements = stringToMap('area,br,col,hr,img,wbr');
// Elements that you can, intentionally, leave open (and which close themselves)
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
- var optionalEndTagBlockElements = toMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'),
- optionalEndTagInlineElements = toMap('rp,rt'),
+ var optionalEndTagBlockElements = stringToMap('colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr'),
+ optionalEndTagInlineElements = stringToMap('rp,rt'),
optionalEndTagElements = extend({},
optionalEndTagInlineElements,
optionalEndTagBlockElements);
// Safe Block Elements - HTML5
- var blockElements = extend({}, optionalEndTagBlockElements, toMap('address,article,' +
+ var blockElements = extend({}, optionalEndTagBlockElements, stringToMap('address,article,' +
'aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5,' +
'h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,section,table,ul'));
// Inline Elements - HTML5
- var inlineElements = extend({}, optionalEndTagInlineElements, toMap('a,abbr,acronym,b,' +
+ var inlineElements = extend({}, optionalEndTagInlineElements, stringToMap('a,abbr,acronym,b,' +
'bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s,' +
'samp,small,span,strike,strong,sub,sup,time,tt,u,var'));
@@ -254,12 +357,12 @@ function $SanitizeProvider() {
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Elements
// Note: the elements animate,animateColor,animateMotion,animateTransform,set are intentionally omitted.
// They can potentially allow for arbitrary javascript to be executed. See #11290
- var svgElements = toMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' +
+ var svgElements = stringToMap('circle,defs,desc,ellipse,font-face,font-face-name,font-face-src,g,glyph,' +
'hkern,image,linearGradient,line,marker,metadata,missing-glyph,mpath,path,polygon,polyline,' +
'radialGradient,rect,stop,svg,switch,text,title,tspan');
// Blocked Elements (will be stripped)
- var blockedElements = toMap('script,style');
+ var blockedElements = stringToMap('script,style');
var validElements = extend({},
voidElements,
@@ -268,9 +371,9 @@ function $SanitizeProvider() {
optionalEndTagElements);
//Attributes that have href and hence need to be sanitized
- var uriAttrs = toMap('background,cite,href,longdesc,src,xlink:href,xml:base');
+ var uriAttrs = stringToMap('background,cite,href,longdesc,src,xlink:href,xml:base');
- var htmlAttrs = toMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
+ var htmlAttrs = stringToMap('abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,' +
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,' +
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,' +
'scope,scrolling,shape,size,span,start,summary,tabindex,target,title,type,' +
@@ -278,7 +381,7 @@ function $SanitizeProvider() {
// SVG attributes (without "id" and "name" attributes)
// https://wiki.whatwg.org/wiki/Sanitization_rules#svg_Attributes
- var svgAttrs = toMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
+ var svgAttrs = stringToMap('accent-height,accumulate,additive,alphabetic,arabic-form,ascent,' +
'baseProfile,bbox,begin,by,calcMode,cap-height,class,color,color-rendering,content,' +
'cx,cy,d,dx,dy,descent,display,dur,end,fill,fill-rule,font-family,font-size,font-stretch,' +
'font-style,font-variant,font-weight,from,fx,fy,g1,g2,glyph-name,gradientUnits,hanging,' +
@@ -299,14 +402,24 @@ function $SanitizeProvider() {
svgAttrs,
htmlAttrs);
- function toMap(str, lowercaseKeys) {
- var obj = {}, items = str.split(','), i;
+ function stringToMap(str, lowercaseKeys) {
+ return arrayToMap(str.split(','), lowercaseKeys);
+ }
+
+ function arrayToMap(items, lowercaseKeys) {
+ var obj = {}, i;
for (i = 0; i < items.length; i++) {
obj[lowercaseKeys ? lowercase(items[i]) : items[i]] = true;
}
return obj;
}
+ function addElementsTo(elementsMap, newElements) {
+ if (newElements && newElements.length) {
+ extend(elementsMap, arrayToMap(newElements));
+ }
+ }
+
/**
* Create an inert document that contains the dirty HTML that needs sanitizing
* Depending upon browser support we use one of three strategies for doing this.
diff --git a/test/ngSanitize/sanitizeSpec.js b/test/ngSanitize/sanitizeSpec.js
index 69cb6abc9fda..a047be989642 100644
--- a/test/ngSanitize/sanitizeSpec.js
+++ b/test/ngSanitize/sanitizeSpec.js
@@ -293,10 +293,56 @@ describe('HTML', function() {
expect(doc).toEqual('
');
}));
+ describe('Custom white-list support', function() {
+
+ var $sanitizeProvider;
+ beforeEach(module(function(_$sanitizeProvider_) {
+ $sanitizeProvider = _$sanitizeProvider_;
+
+ $sanitizeProvider.addValidElements(['foo']);
+ $sanitizeProvider.addValidElements({
+ htmlElements: ['foo-button', 'foo-video'],
+ htmlVoidElements: ['foo-input'],
+ svgElements: ['foo-svg']
+ });
+ $sanitizeProvider.addValidAttrs(['foo']);
+ }));
+
+ it('should allow custom white-listed element', function() {
+ expectHTML('').toEqual('');
+ expectHTML('').toEqual('');
+ expectHTML('').toEqual('');
+ });
+
+ it('should allow custom white-listed void element', function() {
+ expectHTML('').toEqual('');
+ });
+
+ it('should allow custom white-listed void element to be used with closing tag', function() {
+ expectHTML('').toEqual('');
+ });
+
+ it('should allow custom white-listed attribute', function() {
+ expectHTML('').toEqual('');
+ });
+
+ it('should ignore custom white-listed SVG element if SVG disabled', function() {
+ expectHTML('').toEqual('');
+ });
+
+ it('should not allow add custom element after service has been instantiated', inject(function($sanitize) {
+ $sanitizeProvider.addValidElements(['bar']);
+ expectHTML('').toEqual('');
+ }));
+ });
+
describe('SVG support', function() {
beforeEach(module(function($sanitizeProvider) {
$sanitizeProvider.enableSvg(true);
+ $sanitizeProvider.addValidElements({
+ svgElements: ['font-face-uri']
+ });
}));
it('should accept SVG tags', function() {
@@ -314,6 +360,10 @@ describe('HTML', function() {
});
+ it('should allow custom white-listed SVG element', function() {
+ expectHTML('').toEqual('');
+ });
+
it('should sanitize SVG xlink:href attribute values', function() {
expectHTML('')
.toBeOneOf('',
From 02f4ca4887f337e87ce668f657c32f49e18beec8 Mon Sep 17 00:00:00 2001
From: frederikprijck
Date: Mon, 30 Jan 2017 22:46:22 +0100
Subject: [PATCH 180/552] docs(ngClass): add docs regarding animation for
`ngClassEven` and `ngClassOdd`
Previously, the documentation has no information regarding using
`ngAnimate` together with the `ngClassEven` and `ngClassOdd` directives.
This commit adds the same docs used by the `ngClass` directive to the
`ngClassEven` and `ngClassOdd` docs and adds an extra example for both
`ngClassEven` and `ngClassOdd` that showcases animations.
Closes #15654
---
docs/content/guide/animations.ngdoc | 7 +-
src/ng/directive/ngClass.js | 124 ++++++++++++++++++++++++++++
2 files changed, 128 insertions(+), 3 deletions(-)
diff --git a/docs/content/guide/animations.ngdoc b/docs/content/guide/animations.ngdoc
index a13661a36a68..22e4df094839 100644
--- a/docs/content/guide/animations.ngdoc
+++ b/docs/content/guide/animations.ngdoc
@@ -229,11 +229,12 @@ triggered:
| {@link ngRoute.directive:ngView#animations ngView} | enter and leave |
| {@link module:ngMessages#animations ngMessage / ngMessageExp} | enter and leave |
| {@link ng.directive:ngClass#animations ngClass / {{class}}} | add and remove |
-| {@link ng.directive:ngClass#animations ngClassEven / ngClassOdd} | add and remove |
+| {@link ng.directive:ngClassEven#animations ngClassEven} | add and remove |
+| {@link ng.directive:ngClassOdd#animations ngClassOdd} | add and remove |
| {@link ng.directive:ngHide#animations ngHide} | add and remove (the `ng-hide` class) |
| {@link ng.directive:ngShow#animations ngShow} | add and remove (the `ng-hide` class) |
-| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
-| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
+| {@link ng.directive:ngModel#animations ngModel} | add and remove ({@link ng.directive:ngModel#css-classes various classes}) |
+| {@link ng.directive:form#animations form / ngForm} | add and remove ({@link ng.directive:form#css-classes various classes}) |
| {@link module:ngMessages#animations ngMessages} | add and remove (the `ng-active`/`ng-inactive` classes) |
For a full breakdown of the steps involved during each animation event, refer to the
diff --git a/src/ng/directive/ngClass.js b/src/ng/directive/ngClass.js
index e38c7c141938..7b1ca13b2915 100644
--- a/src/ng/directive/ngClass.js
+++ b/src/ng/directive/ngClass.js
@@ -338,6 +338,12 @@ var ngClassDirective = classDirective('', true);
* This directive can be applied only within the scope of an
* {@link ng.directive:ngRepeat ngRepeat}.
*
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
+ *
* @element ANY
* @param {expression} ngClassOdd {@link guide/expression Expression} to eval. The result
* of the evaluation can be a string representing space delimited class names or an array.
@@ -370,6 +376,62 @@ var ngClassDirective = classDirective('', true);
});
+ *
+ *
+ * @example
+ * An example on how to implement animations using `ngClassOdd`:
+ *
+
+
+
+
+
+
+
+
{{ item }}
+
+
+
+
+
+ .odd {
+ background: rgba(255, 255, 0, 0.25);
+ }
+
+ .odd-add, .odd-remove {
+ transition: 1.5s;
+ }
+
+
+ it('should add new entries to the beginning of the list', function() {
+ var button = element(by.buttonText('Add item'));
+ var rows = element.all(by.repeater('item in items'));
+
+ expect(rows.count()).toBe(4);
+ expect(rows.get(0).getText()).toBe('Item 3');
+ expect(rows.get(1).getText()).toBe('Item 2');
+
+ button.click();
+
+ expect(rows.count()).toBe(5);
+ expect(rows.get(0).getText()).toBe('Item 4');
+ expect(rows.get(1).getText()).toBe('Item 3');
+ });
+
+ it('should add odd class to odd entries', function() {
+ var button = element(by.buttonText('Add item'));
+ var rows = element.all(by.repeater('item in items'));
+
+ expect(rows.get(0).getAttribute('class')).toMatch(/odd/);
+ expect(rows.get(1).getAttribute('class')).not.toMatch(/odd/);
+
+ button.click();
+
+ expect(rows.get(0).getAttribute('class')).toMatch(/odd/);
+ expect(rows.get(1).getAttribute('class')).not.toMatch(/odd/);
+ });
+
+
*/
var ngClassOddDirective = classDirective('Odd', 0);
@@ -386,6 +448,12 @@ var ngClassOddDirective = classDirective('Odd', 0);
* This directive can be applied only within the scope of an
* {@link ng.directive:ngRepeat ngRepeat}.
*
+ * @animations
+ * | Animation | Occurs |
+ * |----------------------------------|-------------------------------------|
+ * | {@link ng.$animate#addClass addClass} | just before the class is applied to the element |
+ * | {@link ng.$animate#removeClass removeClass} | just before the class is removed from the element |
+ *
* @element ANY
* @param {expression} ngClassEven {@link guide/expression Expression} to eval. The
* result of the evaluation can be a string representing space delimited class names or an array.
@@ -418,5 +486,61 @@ var ngClassOddDirective = classDirective('Odd', 0);
});
+ *
+ *
+ * @example
+ * An example on how to implement animations using `ngClassEven`:
+ *
+
+
+
');
})
);
diff --git a/test/ng/templateRequestSpec.js b/test/ng/templateRequestSpec.js
index 3ca323613103..23f05f1e8d08 100644
--- a/test/ng/templateRequestSpec.js
+++ b/test/ng/templateRequestSpec.js
@@ -144,9 +144,9 @@ describe('$templateRequest', function() {
$templateRequest('tpl.html').catch(function(reason) { err = reason; });
$httpBackend.flush();
- expect(err).toEqualMinErr('$compile', 'tpload',
+ expect(err).toEqualMinErr('$templateRequest', 'tpload',
'Failed to load template: tpl.html (HTTP status: 404 Not Found)');
- expect($exceptionHandler.errors[0]).toEqualMinErr('$compile', 'tpload',
+ expect($exceptionHandler.errors[0]).toEqualMinErr('$templateRequest', 'tpload',
'Failed to load template: tpl.html (HTTP status: 404 Not Found)');
});
});
diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js
index 772bdc7bc226..36832ab57884 100644
--- a/test/ngRoute/routeSpec.js
+++ b/test/ngRoute/routeSpec.js
@@ -892,7 +892,7 @@ describe('$route', function() {
$httpBackend.flush();
expect($exceptionHandler.errors.pop()).
- toEqualMinErr('$compile', 'tpload', 'Failed to load template: r1.html');
+ toEqualMinErr('$templateRequest', 'tpload', 'Failed to load template: r1.html');
$httpBackend.expectGET('r2.html').respond('');
$location.path('/r2');
From 8b399545a5098cb2576594a26a03cd7268c55fb6 Mon Sep 17 00:00:00 2001
From: Pete Bacon Darwin
Date: Tue, 20 Feb 2018 11:13:38 +0000
Subject: [PATCH 203/552] docs($route): fix typo in error message
---
docs/content/error/$route/norout.ngdoc | 2 +-
src/ngRoute/route.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/docs/content/error/$route/norout.ngdoc b/docs/content/error/$route/norout.ngdoc
index 30a12d151442..5dc5a9b8b7ee 100644
--- a/docs/content/error/$route/norout.ngdoc
+++ b/docs/content/error/$route/norout.ngdoc
@@ -1,6 +1,6 @@
@ngdoc error
@name $route:norout
-@fullName Tried updating route when with no current route
+@fullName Tried updating route with no current route
@description
Occurs when an attempt is made to update the parameters on the current route when
diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js
index 76f915b97da6..f0e6c19b9079 100644
--- a/src/ngRoute/route.js
+++ b/src/ngRoute/route.js
@@ -605,7 +605,7 @@ function $RouteProvider() {
// interpolate modifies newParams, only query params are left
$location.search(newParams);
} else {
- throw $routeMinErr('norout', 'Tried updating route when with no current route');
+ throw $routeMinErr('norout', 'Tried updating route with no current route');
}
}
};
From ea0585773bb93fd891576e2271254a17e15f1ddd Mon Sep 17 00:00:00 2001
From: George Kalpakas
Date: Sat, 10 Feb 2018 22:39:28 +0200
Subject: [PATCH 204/552] fix($resource): fix interceptors and success/error
callbacks
Previously, action-specific interceptors and `success`/`error` callbacks
were executed in inconsistent relative orders and in a way that did not
meet the general expectation for interceptor behavior (e.g. ability to
recover from errors, performing asynchronous operations, etc).
This commit fixes the behavior to make it more consistent and expected.
The main differences are that `success`/`error` callbacks will now be
run _after_ `response`/`responseError` interceptors complete (even if
interceptors return a promise) and the correct callback will be called
based on the result of the interceptor (e.g. if the `responseError`
interceptor recovers from an error, the `success` callback will be
called).
See also https://github.com/angular/angular.js/issues/9334#issuecomment-364650642.
This commit also replaces the use of `success`/`error` callbacks in the
docs with using the returned promise.
Fixes #6731
Fixes #9334
Closes #6865
Closes #16446
BREAKING CHANGE:
If you are not using `success` or `error` callbacks with `$resource`,
your app should not be affected by this change.
If you are using `success` or `error` callbacks (with or without
response interceptors), one (subtle) difference is that throwing an
error inside the callbacks will not propagate to the returned
`$promise`. Therefore, you should try to use the promises whenever
possible. E.g.:
```js
// Avoid
User.query(function onSuccess(users) { throw new Error(); }).
$promise.
catch(function onError() { /* Will not be called. */ });
// Prefer
User.query().
$promise.
then(function onSuccess(users) { throw new Error(); }).
catch(function onError() { /* Will be called. */ });
```
Finally, if you are using `success` or `error` callbacks with response
interceptors, the callbacks will now always run _after_ the interceptors
(and wait for them to resolve in case they return a promise).
Previously, the `error` callback was called before the `responseError`
interceptor and the `success` callback was synchronously called after
the `response` interceptor. E.g.:
```js
var User = $resource('/api/users/:id', {id: '@id'}, {
get: {
method: 'get',
interceptor: {
response: function(response) {
console.log('responseInterceptor-1');
return $timeout(1000).then(function() {
console.log('responseInterceptor-2');
return response.resource;
});
},
responseError: function(response) {
console.log('responseErrorInterceptor-1');
return $timeout(1000).then(function() {
console.log('responseErrorInterceptor-2');
return $q.reject('Ooops!');
});
}
}
}
});
var onSuccess = function(value) { console.log('successCallback', value); };
var onError = function(error) { console.log('errorCallback', error); };
// Assuming the following call is successful...
User.get({id: 1}, onSuccess, onError);
// Old behavior:
// responseInterceptor-1
// successCallback, {/* Promise object */}
// responseInterceptor-2
// New behavior:
// responseInterceptor-1
// responseInterceptor-2
// successCallback, {/* User object */}
// Assuming the following call returns an error...
User.get({id: 2}, onSuccess, onError);
// Old behavior:
// errorCallback, {/* Response object */}
// responseErrorInterceptor-1
// responseErrorInterceptor-2
// New behavior:
// responseErrorInterceptor-1
// responseErrorInterceptor-2
// errorCallback, Ooops!
```
---
src/ngResource/resource.js | 395 ++++++++++++++++++--------------
test/ngResource/resourceSpec.js | 288 +++++++++++++++++------
2 files changed, 441 insertions(+), 242 deletions(-)
diff --git a/src/ngResource/resource.js b/src/ngResource/resource.js
index c8a79274ca2b..11bb45ba20b3 100644
--- a/src/ngResource/resource.js
+++ b/src/ngResource/resource.js
@@ -110,13 +110,13 @@ function shallowClearAndCopy(src, dst) {
*
* @param {Object=} paramDefaults Default values for `url` parameters. These can be overridden in
* `actions` methods. If a parameter value is a function, it will be called every time
- * a param value needs to be obtained for a request (unless the param was overridden). The function
- * will be passed the current data value as an argument.
+ * a param value needs to be obtained for a request (unless the param was overridden). The
+ * function will be passed the current data value as an argument.
*
* Each key value in the parameter object is first bound to url template if present and then any
* excess keys are appended to the url search query after the `?`.
*
- * Given a template `/path/:verb` and parameter `{verb:'greet', salutation:'Hello'}` results in
+ * Given a template `/path/:verb` and parameter `{verb: 'greet', salutation: 'Hello'}` results in
* URL `/path/greet?salutation=Hello`.
*
* If the parameter value is prefixed with `@`, then the value for that parameter will be
@@ -125,7 +125,7 @@ function shallowClearAndCopy(src, dst) {
* For example, if the `defaultParam` object is `{someParam: '@someProp'}` then the value of
* `someParam` will be `data.someProp`.
* Note that the parameter will be ignored, when calling a "GET" action method (i.e. an action
- * method that does not accept a request body)
+ * method that does not accept a request body).
*
* @param {Object.