Skip to content

Commit fe95b10

Browse files
Merge pull request #110 from angular/master
Update upstream
2 parents 5245211 + ebeb1c9 commit fe95b10

File tree

6 files changed

+139
-44
lines changed

6 files changed

+139
-44
lines changed

src/ng/browser.js

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
'use strict';
2-
/* global stripHash: true */
2+
/* global getHash: true, stripHash: false */
3+
4+
function getHash(url) {
5+
var index = url.indexOf('#');
6+
return index === -1 ? '' : url.substr(index);
7+
}
8+
9+
function trimEmptyHash(url) {
10+
return url.replace(/#$/, '');
11+
}
312

413
/**
514
* ! This is a private undocumented service !
@@ -62,30 +71,26 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
6271

6372
cacheState();
6473

65-
function getHash(url) {
66-
var index = url.indexOf('#');
67-
return index === -1 ? '' : url.substr(index);
68-
}
69-
7074
/**
7175
* @name $browser#url
7276
*
7377
* @description
7478
* GETTER:
75-
* Without any argument, this method just returns current value of location.href.
79+
* Without any argument, this method just returns current value of `location.href` (with a
80+
* trailing `#` stripped of if the hash is empty).
7681
*
7782
* SETTER:
7883
* With at least one argument, this method sets url to new value.
79-
* If html5 history api supported, pushState/replaceState is used, otherwise
80-
* location.href/location.replace is used.
81-
* Returns its own instance to allow chaining
84+
* If html5 history api supported, `pushState`/`replaceState` is used, otherwise
85+
* `location.href`/`location.replace` is used.
86+
* Returns its own instance to allow chaining.
8287
*
83-
* NOTE: this api is intended for use only by the $location service. Please use the
88+
* NOTE: this api is intended for use only by the `$location` service. Please use the
8489
* {@link ng.$location $location service} to change url.
8590
*
8691
* @param {string} url New url (when used as setter)
8792
* @param {boolean=} replace Should new url replace current history record?
88-
* @param {object=} state object to use with pushState/replaceState
93+
* @param {object=} state State object to use with `pushState`/`replaceState`
8994
*/
9095
self.url = function(url, replace, state) {
9196
// In modern browsers `history.state` is `null` by default; treating it separately
@@ -143,7 +148,7 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) {
143148
// - pendingLocation is needed as browsers don't allow to read out
144149
// the new location.href if a reload happened or if there is a bug like in iOS 9 (see
145150
// https://openradar.appspot.com/22186109).
146-
return pendingLocation || location.href;
151+
return trimEmptyHash(pendingLocation || location.href);
147152
}
148153
};
149154

src/ng/location.js

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
'use strict';
2+
/* global stripHash: true */
23

34
var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/,
45
DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21};
@@ -94,17 +95,11 @@ function stripBaseUrl(base, url) {
9495
}
9596
}
9697

97-
9898
function stripHash(url) {
9999
var index = url.indexOf('#');
100100
return index === -1 ? url : url.substr(0, index);
101101
}
102102

103-
function trimEmptyHash(url) {
104-
return url.replace(/(#.+)|#$/, '$1');
105-
}
106-
107-
108103
function stripFile(url) {
109104
return url.substr(0, stripHash(url).lastIndexOf('/') + 1);
110105
}
@@ -943,7 +938,7 @@ function $LocationProvider() {
943938

944939

945940
// rewrite hashbang url <> html5 url
946-
if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) {
941+
if ($location.absUrl() !== initialUrl) {
947942
$browser.url($location.absUrl(), true);
948943
}
949944

@@ -962,7 +957,6 @@ function $LocationProvider() {
962957
var oldUrl = $location.absUrl();
963958
var oldState = $location.$$state;
964959
var defaultPrevented;
965-
newUrl = trimEmptyHash(newUrl);
966960
$location.$$parse(newUrl);
967961
$location.$$state = newState;
968962

@@ -990,8 +984,8 @@ function $LocationProvider() {
990984
if (initializing || $location.$$urlUpdatedByLocation) {
991985
$location.$$urlUpdatedByLocation = false;
992986

993-
var oldUrl = trimEmptyHash($browser.url());
994-
var newUrl = trimEmptyHash($location.absUrl());
987+
var oldUrl = $browser.url();
988+
var newUrl = $location.absUrl();
995989
var oldState = $browser.state();
996990
var currentReplace = $location.$$replace;
997991
var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) ||

src/ngMock/angular-mocks.js

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,8 @@ angular.mock.$Browser.prototype = {
225225
state = null;
226226
}
227227
if (url) {
228-
this.$$url = url;
228+
// The `$browser` service trims empty hashes; simulate it.
229+
this.$$url = url.replace(/#$/, '');
229230
// Native pushState serializes & copies the object; simulate it.
230231
this.$$state = angular.copy(state);
231232
return this;
@@ -1510,16 +1511,25 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
15101511
}
15111512
}
15121513

1514+
function createFatalError(message) {
1515+
var error = new Error(message);
1516+
// In addition to being converted to a rejection, these errors also need to be passed to
1517+
// the $exceptionHandler and be rethrown (so that the test fails).
1518+
error.$$passToExceptionHandler = true;
1519+
return error;
1520+
}
1521+
15131522
if (expectation && expectation.match(method, url)) {
15141523
if (!expectation.matchData(data)) {
1515-
throw new Error('Expected ' + expectation + ' with different data\n' +
1516-
'EXPECTED: ' + prettyPrint(expectation.data) + '\nGOT: ' + data);
1524+
throw createFatalError('Expected ' + expectation + ' with different data\n' +
1525+
'EXPECTED: ' + prettyPrint(expectation.data) + '\n' +
1526+
'GOT: ' + data);
15171527
}
15181528

15191529
if (!expectation.matchHeaders(headers)) {
1520-
throw new Error('Expected ' + expectation + ' with different headers\n' +
1521-
'EXPECTED: ' + prettyPrint(expectation.headers) + '\nGOT: ' +
1522-
prettyPrint(headers));
1530+
throw createFatalError('Expected ' + expectation + ' with different headers\n' +
1531+
'EXPECTED: ' + prettyPrint(expectation.headers) + '\n' +
1532+
'GOT: ' + prettyPrint(headers));
15231533
}
15241534

15251535
expectations.shift();
@@ -1540,20 +1550,17 @@ function createHttpBackendMock($rootScope, $timeout, $delegate, $browser) {
15401550
($browser ? $browser.defer : responsesPush)(wrapResponse(definition));
15411551
} else if (definition.passThrough) {
15421552
originalHttpBackend(method, url, data, callback, headers, timeout, withCredentials, responseType, eventHandlers, uploadEventHandlers);
1543-
} else throw new Error('No response defined !');
1553+
} else throw createFatalError('No response defined !');
15441554
return;
15451555
}
15461556
}
1547-
var error = wasExpected ?
1548-
new Error('No response defined !') :
1549-
new Error('Unexpected request: ' + method + ' ' + url + '\n' +
1550-
(expectation ? 'Expected ' + expectation : 'No more request expected'));
15511557

1552-
// In addition to be being converted to a rejection, this error also needs to be passed to
1553-
// the $exceptionHandler and be rethrown (so that the test fails).
1554-
error.$$passToExceptionHandler = true;
1558+
if (wasExpected) {
1559+
throw createFatalError('No response defined !');
1560+
}
15551561

1556-
throw error;
1562+
throw createFatalError('Unexpected request: ' + method + ' ' + url + '\n' +
1563+
(expectation ? 'Expected ' + expectation : 'No more request expected'));
15571564
}
15581565

15591566
/**

test/ng/browserSpecs.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,14 @@ describe('browser', function() {
404404
expect(browser.url()).toEqual('https://another.com');
405405
});
406406

407+
it('should strip an empty hash fragment', function() {
408+
fakeWindow.location.href = 'http://test.com#';
409+
expect(browser.url()).toEqual('http://test.com');
410+
411+
fakeWindow.location.href = 'https://another.com#foo';
412+
expect(browser.url()).toEqual('https://another.com#foo');
413+
});
414+
407415
it('should use history.pushState when available', function() {
408416
sniffer.history = true;
409417
browser.url('http://new.org');
@@ -1047,6 +1055,32 @@ describe('browser', function() {
10471055
expect($location.absUrl()).toEqual('http://server/#otherHash');
10481056
});
10491057
});
1058+
1059+
// issue #16632
1060+
it('should not trigger `$locationChangeStart` more than once due to trailing `#`', function() {
1061+
setup({
1062+
history: true,
1063+
html5Mode: true
1064+
});
1065+
1066+
inject(function($flushPendingTasks, $location, $rootScope) {
1067+
$rootScope.$digest();
1068+
1069+
var spy = jasmine.createSpy('$locationChangeStart');
1070+
$rootScope.$on('$locationChangeStart', spy);
1071+
1072+
$rootScope.$evalAsync(function() {
1073+
fakeWindow.location.href += '#';
1074+
});
1075+
$rootScope.$digest();
1076+
1077+
expect(fakeWindow.location.href).toBe('http://server/#');
1078+
expect($location.absUrl()).toBe('http://server/');
1079+
1080+
expect(spy.calls.count()).toBe(0);
1081+
expect(spy).not.toHaveBeenCalled();
1082+
});
1083+
});
10501084
});
10511085

10521086
describe('integration test with $rootScope', function() {

test/ng/locationSpec.js

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -693,10 +693,10 @@ describe('$location', function() {
693693

694694
describe('location watch', function() {
695695

696-
it('should not update browser if only the empty hash fragment is cleared by updating the search', function() {
696+
it('should not update browser if only the empty hash fragment is cleared', function() {
697697
initService({supportHistory: true});
698-
mockUpBrowser({initialUrl:'http://new.com/a/b#', baseHref:'/base/'});
699-
inject(function($rootScope, $browser, $location) {
698+
mockUpBrowser({initialUrl: 'http://new.com/a/b#', baseHref: '/base/'});
699+
inject(function($browser, $rootScope) {
700700
$browser.url('http://new.com/a/b');
701701
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough();
702702
$rootScope.$digest();
@@ -707,10 +707,11 @@ describe('$location', function() {
707707

708708
it('should not replace browser url if only the empty hash fragment is cleared', function() {
709709
initService({html5Mode: true, supportHistory: true});
710-
mockUpBrowser({initialUrl:'http://new.com/#', baseHref: '/'});
711-
inject(function($browser, $location) {
712-
expect($browser.url()).toBe('http://new.com/#');
710+
mockUpBrowser({initialUrl: 'http://new.com/#', baseHref: '/'});
711+
inject(function($browser, $location, $window) {
712+
expect($browser.url()).toBe('http://new.com/');
713713
expect($location.absUrl()).toBe('http://new.com/');
714+
expect($window.location.href).toBe('http://new.com/#');
714715
});
715716
});
716717

test/ngMock/angular-mocksSpec.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1506,6 +1506,42 @@ describe('ngMock', function() {
15061506
});
15071507

15081508

1509+
it('should throw error when expectation fails', function() {
1510+
expect(function() {
1511+
hb.expectPOST('/some', {foo: 1}).respond({});
1512+
hb('POST', '/some', {foo: 2}, callback);
1513+
hb.flush();
1514+
}).toThrowError(/^Expected POST \/some with different data/);
1515+
});
1516+
1517+
1518+
it('should throw error when expectation about headers fails', function() {
1519+
expect(function() {
1520+
hb.expectPOST('/some', {foo: 1}, {X: 'val1'}).respond({});
1521+
hb('POST', '/some', {foo: 1}, callback, {X: 'val2'});
1522+
hb.flush();
1523+
}).toThrowError(/^Expected POST \/some with different headers/);
1524+
});
1525+
1526+
1527+
it('should throw error about data when expectations about both data and headers fail', function() {
1528+
expect(function() {
1529+
hb.expectPOST('/some', {foo: 1}, {X: 'val1'}).respond({});
1530+
hb('POST', '/some', {foo: 2}, callback, {X: 'val2'});
1531+
hb.flush();
1532+
}).toThrowError(/^Expected POST \/some with different data/);
1533+
});
1534+
1535+
1536+
it('should throw error when response is not defined for a backend definition', function() {
1537+
expect(function() {
1538+
hb.whenGET('/some'); // no .respond(...) !
1539+
hb('GET', '/some', null, callback);
1540+
hb.flush();
1541+
}).toThrowError('No response defined !');
1542+
});
1543+
1544+
15091545
it('should match headers if specified', function() {
15101546
hb.when('GET', '/url', null, {'X': 'val1'}).respond(201, 'content1');
15111547
hb.when('GET', '/url', null, {'X': 'val2'}).respond(202, 'content2');
@@ -2833,6 +2869,24 @@ describe('ngMockE2E', function() {
28332869
}).toThrowError('Unexpected request: GET /some\nNo more request expected');
28342870
});
28352871

2872+
it('should throw error when expectation fails - without error callback', function() {
2873+
expect(function() {
2874+
hb.expectPOST('/some', { foo: 1 }).respond({});
2875+
$http.post('/some', { foo: 2 }).then(noop);
2876+
2877+
hb.flush();
2878+
}).toThrowError(/^Expected POST \/some with different data/);
2879+
});
2880+
2881+
it('should throw error when unexpected request - with error callback', function() {
2882+
expect(function() {
2883+
hb.expectPOST('/some', { foo: 1 }).respond({});
2884+
$http.post('/some', { foo: 2 }).then(noop, noop);
2885+
2886+
hb.flush();
2887+
}).toThrowError(/^Expected POST \/some with different data/);
2888+
});
2889+
28362890

28372891
describe('passThrough()', function() {
28382892
it('should delegate requests to the real backend when passThrough is invoked', function() {

0 commit comments

Comments
 (0)