From 7f524c4db39ee3b0b1a253461dae945585954f2b Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 19 Jul 2018 18:10:11 +0300 Subject: [PATCH 1/9] refactor($location): minor changes (unused deps, exported globals, unused deps, etc) --- src/ng/location.js | 1 + test/ng/locationSpec.js | 10 +++++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/ng/location.js b/src/ng/location.js index cc064becd727..62174412b064 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -1,4 +1,5 @@ 'use strict'; +/* global stripHash: true */ var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index 58d4d013d968..c4b08be83bae 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -693,10 +693,10 @@ describe('$location', function() { describe('location watch', function() { - it('should not update browser if only the empty hash fragment is cleared by updating the search', function() { + it('should not update browser if only the empty hash fragment is cleared', function() { initService({supportHistory: true}); - mockUpBrowser({initialUrl:'http://new.com/a/b#', baseHref:'/base/'}); - inject(function($rootScope, $browser, $location) { + mockUpBrowser({initialUrl: 'http://new.com/a/b#', baseHref: '/base/'}); + inject(function($browser, $rootScope) { $browser.url('http://new.com/a/b'); var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').and.callThrough(); $rootScope.$digest(); @@ -707,8 +707,8 @@ describe('$location', function() { it('should not replace browser url if only the empty hash fragment is cleared', function() { initService({html5Mode: true, supportHistory: true}); - mockUpBrowser({initialUrl:'http://new.com/#', baseHref: '/'}); - inject(function($browser, $location) { + mockUpBrowser({initialUrl: 'http://new.com/#', baseHref: '/'}); + inject(function($browser, $location, $window) { expect($browser.url()).toBe('http://new.com/#'); expect($location.absUrl()).toBe('http://new.com/'); }); From 8f28de49265a59f59b5fb6011d1c494564f43a2f Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 19 Jul 2018 18:11:56 +0300 Subject: [PATCH 2/9] refactor($location): remove unnecessary capturing group in RegExp --- src/ng/location.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ng/location.js b/src/ng/location.js index 62174412b064..098309520c01 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -102,7 +102,7 @@ function stripHash(url) { } function trimEmptyHash(url) { - return url.replace(/(#.+)|#$/, '$1'); + return url.replace(/#$/, ''); } From 8053bc48537b77a6b6ea7375a9d34c87a25746bd Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 19 Jul 2018 19:20:33 +0300 Subject: [PATCH 3/9] refactor($browser): correctly export helper used in specs The helper is used in `fakeWindow.location.hash`. ATM, no test is using the `hash` getter, so there were no errors. --- src/ng/browser.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index 2f1494c905bf..a47a6c09f496 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -1,5 +1,10 @@ 'use strict'; -/* global stripHash: true */ +/* global getHash: true, stripHash: false */ + +function getHash(url) { + var index = url.indexOf('#'); + return index === -1 ? '' : url.substr(index); +} /** * ! This is a private undocumented service ! @@ -62,11 +67,6 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) { cacheState(); - function getHash(url) { - var index = url.indexOf('#'); - return index === -1 ? '' : url.substr(index); - } - /** * @name $browser#url * From bbf6900181917e190dad278e7022d1fc1e017324 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 19 Jul 2018 18:11:06 +0300 Subject: [PATCH 4/9] test($location): add assertion --- test/ng/locationSpec.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index c4b08be83bae..c187930dad39 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -711,6 +711,7 @@ describe('$location', function() { inject(function($browser, $location, $window) { expect($browser.url()).toBe('http://new.com/#'); expect($location.absUrl()).toBe('http://new.com/'); + expect($window.location.href).toBe('http://new.com/#'); }); }); From b085893e9ed83cc654f1adacb10aff71db79739a Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 19 Jul 2018 19:14:57 +0300 Subject: [PATCH 5/9] fix($location): avoid unnecessary `$locationChange*` events due to empty hash Fixes #16632 --- src/ng/browser.js | 4 ++-- src/ng/location.js | 2 +- test/ng/browserSpecs.js | 34 ++++++++++++++++++++++++++++++++++ test/ng/locationSpec.js | 2 +- 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index a47a6c09f496..4c2e200452b2 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -1,5 +1,5 @@ 'use strict'; -/* global getHash: true, stripHash: false */ +/* global getHash: true, stripHash: false, trimEmptyHash: false */ function getHash(url) { var index = url.indexOf('#'); @@ -143,7 +143,7 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) { // - pendingLocation is needed as browsers don't allow to read out // the new location.href if a reload happened or if there is a bug like in iOS 9 (see // https://openradar.appspot.com/22186109). - return pendingLocation || location.href; + return pendingLocation || trimEmptyHash(location.href); } }; diff --git a/src/ng/location.js b/src/ng/location.js index 098309520c01..44cb4df1b63b 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -1,5 +1,5 @@ 'use strict'; -/* global stripHash: true */ +/* global stripHash: true, trimEmptyHash: true */ var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index acaf63e18d23..23b32addca27 100644 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -404,6 +404,14 @@ describe('browser', function() { expect(browser.url()).toEqual('https://another.com'); }); + it('should strip an empty hash fragment', function() { + fakeWindow.location.href = 'http://test.com#'; + expect(browser.url()).toEqual('http://test.com'); + + fakeWindow.location.href = 'https://another.com#foo'; + expect(browser.url()).toEqual('https://another.com#foo'); + }); + it('should use history.pushState when available', function() { sniffer.history = true; browser.url('http://new.org'); @@ -1047,6 +1055,32 @@ describe('browser', function() { expect($location.absUrl()).toEqual('http://server/#otherHash'); }); }); + + // issue #16632 + it('should not trigger `$locationChangeStart` more than once due to trailing `#`', function() { + setup({ + history: true, + html5Mode: true + }); + + inject(function($flushPendingTasks, $location, $rootScope) { + $rootScope.$digest(); + + const spy = jasmine.createSpy('$locationChangeStart'); + $rootScope.$on('$locationChangeStart', spy); + + $rootScope.$evalAsync(function() { + fakeWindow.location.href += '#'; + }); + $rootScope.$digest(); + + expect(fakeWindow.location.href).toBe('http://server/#'); + expect($location.absUrl()).toBe('http://server/'); + + expect(spy.calls.count()).toBe(0); + expect(spy).not.toHaveBeenCalled(); + }); + }); }); describe('integration test with $rootScope', function() { diff --git a/test/ng/locationSpec.js b/test/ng/locationSpec.js index c187930dad39..e5719ae84dc0 100644 --- a/test/ng/locationSpec.js +++ b/test/ng/locationSpec.js @@ -709,7 +709,7 @@ describe('$location', function() { initService({html5Mode: true, supportHistory: true}); mockUpBrowser({initialUrl: 'http://new.com/#', baseHref: '/'}); inject(function($browser, $location, $window) { - expect($browser.url()).toBe('http://new.com/#'); + expect($browser.url()).toBe('http://new.com/'); expect($location.absUrl()).toBe('http://new.com/'); expect($window.location.href).toBe('http://new.com/#'); }); From c37c2e4656c75b375cbe7b3bb0f456bf045a1a41 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Wed, 25 Jul 2018 22:37:34 +0300 Subject: [PATCH 6/9] fixup! fix($location): avoid unnecessary `$locationChange*` events due to empty hash --- src/ng/browser.js | 8 ++++++-- src/ng/location.js | 15 ++++----------- src/ngMock/angular-mocks.js | 4 ++-- 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index 4c2e200452b2..1f551bdc6fcc 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -1,11 +1,15 @@ 'use strict'; -/* global getHash: true, stripHash: false, trimEmptyHash: false */ +/* global getHash: true, stripHash: false, trimEmptyHash: true */ function getHash(url) { var index = url.indexOf('#'); return index === -1 ? '' : url.substr(index); } +function trimEmptyHash(url) { + return url.replace(/#$/, ''); +} + /** * ! This is a private undocumented service ! * @@ -143,7 +147,7 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) { // - pendingLocation is needed as browsers don't allow to read out // the new location.href if a reload happened or if there is a bug like in iOS 9 (see // https://openradar.appspot.com/22186109). - return pendingLocation || trimEmptyHash(location.href); + return trimEmptyHash(pendingLocation || location.href); } }; diff --git a/src/ng/location.js b/src/ng/location.js index 44cb4df1b63b..3904c7290e77 100644 --- a/src/ng/location.js +++ b/src/ng/location.js @@ -1,5 +1,5 @@ 'use strict'; -/* global stripHash: true, trimEmptyHash: true */ +/* global stripHash: true */ var PATH_MATCH = /^([^?#]*)(\?([^#]*))?(#(.*))?$/, DEFAULT_PORTS = {'http': 80, 'https': 443, 'ftp': 21}; @@ -95,17 +95,11 @@ function stripBaseUrl(base, url) { } } - function stripHash(url) { var index = url.indexOf('#'); return index === -1 ? url : url.substr(0, index); } -function trimEmptyHash(url) { - return url.replace(/#$/, ''); -} - - function stripFile(url) { return url.substr(0, stripHash(url).lastIndexOf('/') + 1); } @@ -944,7 +938,7 @@ function $LocationProvider() { // rewrite hashbang url <> html5 url - if (trimEmptyHash($location.absUrl()) !== trimEmptyHash(initialUrl)) { + if ($location.absUrl() !== initialUrl) { $browser.url($location.absUrl(), true); } @@ -963,7 +957,6 @@ function $LocationProvider() { var oldUrl = $location.absUrl(); var oldState = $location.$$state; var defaultPrevented; - newUrl = trimEmptyHash(newUrl); $location.$$parse(newUrl); $location.$$state = newState; @@ -991,8 +984,8 @@ function $LocationProvider() { if (initializing || $location.$$urlUpdatedByLocation) { $location.$$urlUpdatedByLocation = false; - var oldUrl = trimEmptyHash($browser.url()); - var newUrl = trimEmptyHash($location.absUrl()); + var oldUrl = $browser.url(); + var newUrl = $location.absUrl(); var oldState = $browser.state(); var currentReplace = $location.$$replace; var urlOrStateChanged = !urlsEqual(oldUrl, newUrl) || diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index 0352bac958c3..e8973a535f98 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1,6 +1,6 @@ 'use strict'; -/* global routeToRegExp: false */ +/* global routeToRegExp: false, trimEmptyHash: false */ /** * @ngdoc object @@ -225,7 +225,7 @@ angular.mock.$Browser.prototype = { state = null; } if (url) { - this.$$url = url; + this.$$url = trimEmptyHash(url); // Native pushState serializes & copies the object; simulate it. this.$$state = angular.copy(state); return this; From cdd1f42bd1c6af727fa4af99410d088896c967e9 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Wed, 25 Jul 2018 23:50:57 +0300 Subject: [PATCH 7/9] fixup! fix($location): avoid unnecessary `$locationChange*` events due to empty hash --- test/ng/browserSpecs.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index 23b32addca27..07f3683b76ad 100644 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -1066,7 +1066,7 @@ describe('browser', function() { inject(function($flushPendingTasks, $location, $rootScope) { $rootScope.$digest(); - const spy = jasmine.createSpy('$locationChangeStart'); + var spy = jasmine.createSpy('$locationChangeStart'); $rootScope.$on('$locationChangeStart', spy); $rootScope.$evalAsync(function() { From d736771686e672bb05e94e40a47c9b4b64a05050 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Thu, 26 Jul 2018 15:14:37 +0300 Subject: [PATCH 8/9] fixup! fix($location): avoid unnecessary `$locationChange*` events due to empty hash --- src/ng/browser.js | 2 +- src/ngMock/angular-mocks.js | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index 1f551bdc6fcc..30393754f9a3 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -1,5 +1,5 @@ 'use strict'; -/* global getHash: true, stripHash: false, trimEmptyHash: true */ +/* global getHash: true, stripHash: false */ function getHash(url) { var index = url.indexOf('#'); diff --git a/src/ngMock/angular-mocks.js b/src/ngMock/angular-mocks.js index e8973a535f98..20dc9b2e8c9b 100644 --- a/src/ngMock/angular-mocks.js +++ b/src/ngMock/angular-mocks.js @@ -1,6 +1,6 @@ 'use strict'; -/* global routeToRegExp: false, trimEmptyHash: false */ +/* global routeToRegExp: false */ /** * @ngdoc object @@ -225,7 +225,8 @@ angular.mock.$Browser.prototype = { state = null; } if (url) { - this.$$url = trimEmptyHash(url); + // The `$browser` service trims empty hashes; simulate it. + this.$$url = url.replace(/#$/, ''); // Native pushState serializes & copies the object; simulate it. this.$$state = angular.copy(state); return this; From c3ca532bc835cb2abefd171fb6a17476133b1e00 Mon Sep 17 00:00:00 2001 From: George Kalpakas Date: Fri, 27 Jul 2018 16:26:37 +0300 Subject: [PATCH 9/9] fixup! fix($location): avoid unnecessary `$locationChange*` events due to empty hash --- src/ng/browser.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/ng/browser.js b/src/ng/browser.js index 30393754f9a3..699602bc9b2d 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -76,20 +76,21 @@ function Browser(window, document, $log, $sniffer, $$taskTrackerFactory) { * * @description * GETTER: - * Without any argument, this method just returns current value of location.href. + * Without any argument, this method just returns current value of `location.href` (with a + * trailing `#` stripped of if the hash is empty). * * SETTER: * With at least one argument, this method sets url to new value. - * If html5 history api supported, pushState/replaceState is used, otherwise - * location.href/location.replace is used. - * Returns its own instance to allow chaining + * If html5 history api supported, `pushState`/`replaceState` is used, otherwise + * `location.href`/`location.replace` is used. + * Returns its own instance to allow chaining. * - * NOTE: this api is intended for use only by the $location service. Please use the + * NOTE: this api is intended for use only by the `$location` service. Please use the * {@link ng.$location $location service} to change url. * * @param {string} url New url (when used as setter) * @param {boolean=} replace Should new url replace current history record? - * @param {object=} state object to use with pushState/replaceState + * @param {object=} state State object to use with `pushState`/`replaceState` */ self.url = function(url, replace, state) { // In modern browsers `history.state` is `null` by default; treating it separately