Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a99c8c6

Browse files
committedOct 7, 2014
feat($location): add support for History API state handling
Adds $location state method allowing to get/set a History API state via pushState & replaceState methods. Note that: - Angular treats states undefined and null as the same; trying to change one to the other without touching the URL won't do anything. This is necessary to prevent infinite digest loops when setting the URL to itself in IE<10 in the HTML5 hash fallback mode. - The state() method is not compatible with browsers not supporting the HTML5 History API, e.g. IE 9 or Android < 4.0. Closes #9027
1 parent e843ae7 commit a99c8c6

File tree

8 files changed

+459
-57
lines changed

8 files changed

+459
-57
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
@ngdoc error
2+
@name $location:nostate
3+
@fullName History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API
4+
@description
5+
6+
This error occurs when the {@link ng.$location#state $location.state} method is used when {@link ng.$locationProvider#html5Mode $locationProvider.html5Mode} is not turned on or the browser used doesn't support the HTML5 History API (for example, IE9 or Android 2.3).
7+
8+
To avoid this error, either drop support for those older browsers or avoid using this method.

‎src/ng/browser.js

+37-10
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ function Browser(window, document, $log, $sniffer) {
124124
//////////////////////////////////////////////////////////////
125125

126126
var lastBrowserUrl = location.href,
127+
lastHistoryState = history.state,
127128
baseElement = document.find('base'),
128129
newLocation = null;
129130

@@ -144,27 +145,37 @@ function Browser(window, document, $log, $sniffer) {
144145
* {@link ng.$location $location service} to change url.
145146
*
146147
* @param {string} url New url (when used as setter)
147-
* @param {boolean=} replace Should new url replace current history record ?
148+
* @param {boolean=} replace Should new url replace current history record?
149+
* @param {object=} state object to use with pushState/replaceState
148150
*/
149-
self.url = function(url, replace) {
151+
self.url = function(url, replace, state) {
152+
// In modern browsers `history.state` is `null` by default; treating it separately
153+
// from `undefined` would cause `$browser.url('/foo')` to change `history.state`
154+
// to undefined via `pushState`. Instead, let's change `undefined` to `null` here.
155+
if (isUndefined(state)) {
156+
state = null;
157+
}
158+
150159
// Android Browser BFCache causes location, history reference to become stale.
151160
if (location !== window.location) location = window.location;
152161
if (history !== window.history) history = window.history;
153162

154163
// setter
155164
if (url) {
156-
if (lastBrowserUrl == url) return;
165+
// Prevent IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
166+
// See https://github.com/angular/angular.js/commit/ffb2701
167+
if (lastBrowserUrl === url && (!$sniffer.history || history.state === state)) {
168+
return;
169+
}
157170
var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
158171
lastBrowserUrl = url;
159172
// Don't use history API if only the hash changed
160173
// due to a bug in IE10/IE11 which leads
161174
// to not firing a `hashchange` nor `popstate` event
162175
// in some cases (see #9143).
163-
if (!sameBase && $sniffer.history) {
164-
if (replace) history.replaceState(null, '', url);
165-
else {
166-
history.pushState(null, '', url);
167-
}
176+
if ($sniffer.history && (!sameBase || history.state !== state)) {
177+
history[replace ? 'replaceState' : 'pushState'](state, '', url);
178+
lastHistoryState = history.state;
168179
} else {
169180
newLocation = url;
170181
if (replace) {
@@ -183,6 +194,20 @@ function Browser(window, document, $log, $sniffer) {
183194
}
184195
};
185196

197+
/**
198+
* @name $browser#state
199+
*
200+
* @description
201+
* This method is a getter.
202+
*
203+
* Return history.state or null if history.state is undefined.
204+
*
205+
* @returns {object} state
206+
*/
207+
self.state = function() {
208+
return isUndefined(history.state) ? null : history.state;
209+
};
210+
186211
var urlChangeListeners = [],
187212
urlChangeInit = false;
188213

@@ -192,11 +217,13 @@ function Browser(window, document, $log, $sniffer) {
192217
}
193218

194219
function checkUrlChange() {
195-
if (lastBrowserUrl == self.url()) return;
220+
if (lastBrowserUrl === self.url() && lastHistoryState === history.state) {
221+
return;
222+
}
196223

197224
lastBrowserUrl = self.url();
198225
forEach(urlChangeListeners, function(listener) {
199-
listener(self.url());
226+
listener(self.url(), history.state);
200227
});
201228
}
202229

‎src/ng/location.js

+106-27
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,7 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
303303
}
304304

305305

306-
LocationHashbangInHtml5Url.prototype =
307-
LocationHashbangUrl.prototype =
308-
LocationHtml5Url.prototype = {
306+
var locationPrototype = {
309307

310308
/**
311309
* Are we in html5 mode?
@@ -314,7 +312,7 @@ LocationHashbangInHtml5Url.prototype =
314312
$$html5: false,
315313

316314
/**
317-
* Has any change been replacing ?
315+
* Has any change been replacing?
318316
* @private
319317
*/
320318
$$replace: false,
@@ -530,6 +528,47 @@ LocationHashbangInHtml5Url.prototype =
530528
}
531529
};
532530

531+
forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) {
532+
Location.prototype = Object.create(locationPrototype);
533+
534+
/**
535+
* @ngdoc method
536+
* @name $location#state
537+
*
538+
* @description
539+
* This method is getter / setter.
540+
*
541+
* Return the history state object when called without any parameter.
542+
*
543+
* Change the history state object when called with one parameter and return `$location`.
544+
* The state object is later passed to `pushState` or `replaceState`.
545+
*
546+
* NOTE: This method is supported only in HTML5 mode and only in browsers supporting
547+
* the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
548+
* older browsers (like IE9 or Android < 4.0), don't use this method.
549+
*
550+
* @param {object=} state State object for pushState or replaceState
551+
* @return {object} state
552+
*/
553+
Location.prototype.state = function(state) {
554+
if (!arguments.length)
555+
return this.$$state;
556+
557+
if (Location !== LocationHtml5Url || !this.$$html5) {
558+
throw $locationMinErr('nostate', 'History API state support is available only ' +
559+
'in HTML5 mode and only in browsers supporting HTML5 History API');
560+
}
561+
// The user might modify `stateObject` after invoking `$location.state(stateObject)`
562+
// but we're changing the $$state reference to $browser.state() during the $digest
563+
// so the modification window is narrow.
564+
this.$$state = isUndefined(state) ? null : state;
565+
this.$$stateSent = false; // has the state change been sent to $browser?
566+
567+
return this;
568+
};
569+
});
570+
571+
533572
function locationGetter(property) {
534573
return function() {
535574
return this[property];
@@ -649,9 +688,14 @@ function $LocationProvider(){
649688
* details about event object. Upon successful change
650689
* {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired.
651690
*
691+
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
692+
* the browser supports the HTML5 History API.
693+
*
652694
* @param {Object} angularEvent Synthetic event object.
653695
* @param {string} newUrl New URL
654696
* @param {string=} oldUrl URL that was before it was changed.
697+
* @param {string=} newState New history state object
698+
* @param {string=} oldState History state object that was before it was changed.
655699
*/
656700

657701
/**
@@ -661,9 +705,14 @@ function $LocationProvider(){
661705
* @description
662706
* Broadcasted after a URL was changed.
663707
*
708+
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
709+
* the browser supports the HTML5 History API.
710+
*
664711
* @param {Object} angularEvent Synthetic event object.
665712
* @param {string} newUrl New URL
666713
* @param {string=} oldUrl URL that was before it was changed.
714+
* @param {string=} newState New history state object
715+
* @param {string=} oldState History state object that was before it was changed.
667716
*/
668717

669718
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
@@ -688,8 +737,30 @@ function $LocationProvider(){
688737
$location = new LocationMode(appBase, '#' + hashPrefix);
689738
$location.$$parseLinkUrl(initialUrl, initialUrl);
690739

740+
$location.$$state = $browser.state();
741+
$location.$$stateSent = true; // we're already in sync with $browser
742+
691743
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
692744

745+
function setBrowserUrlWithFallback(url, replace, state) {
746+
var oldUrl = $location.url();
747+
var oldState = $location.$$state;
748+
try {
749+
$browser.url(url, replace, state);
750+
751+
// Make sure $location.state() returns referentially identical (not just deeply equal)
752+
// state object; this makes possible quick checking if the state changed in the digest
753+
// loop. Checking deep equality would be too expensive.
754+
$location.$$state = $browser.state();
755+
} catch (e) {
756+
// Restore old values if pushState fails
757+
$location.url(oldUrl);
758+
$location.$$state = oldState;
759+
760+
throw e;
761+
}
762+
}
763+
693764
$rootElement.on('click', function(event) {
694765
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
695766
// currently we open nice url link and redirect then
@@ -741,51 +812,59 @@ function $LocationProvider(){
741812
}
742813

743814
// update $location when $browser url changes
744-
$browser.onUrlChange(function(newUrl) {
745-
if ($location.absUrl() != newUrl) {
746-
$rootScope.$evalAsync(function() {
747-
var oldUrl = $location.absUrl();
748-
749-
$location.$$parse(newUrl);
750-
if ($rootScope.$broadcast('$locationChangeStart', newUrl,
751-
oldUrl).defaultPrevented) {
752-
$location.$$parse(oldUrl);
753-
$browser.url(oldUrl);
754-
} else {
755-
afterLocationChange(oldUrl);
756-
}
757-
});
758-
if (!$rootScope.$$phase) $rootScope.$digest();
759-
}
815+
$browser.onUrlChange(function(newUrl, newState) {
816+
$rootScope.$evalAsync(function() {
817+
var oldUrl = $location.absUrl();
818+
var oldState = $location.$$state;
819+
820+
$location.$$parse(newUrl);
821+
$location.$$state = newState;
822+
if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
823+
newState, oldState).defaultPrevented) {
824+
$location.$$parse(oldUrl);
825+
$location.$$state = oldState;
826+
setBrowserUrlWithFallback(oldUrl, false, oldState);
827+
} else {
828+
afterLocationChange(oldUrl, oldState);
829+
}
830+
});
831+
if (!$rootScope.$$phase) $rootScope.$digest();
760832
});
761833

762834
// update browser
763835
var changeCounter = 0;
764836
$rootScope.$watch(function $locationWatch() {
765837
var oldUrl = $browser.url();
838+
var oldState = $browser.state();
766839
var currentReplace = $location.$$replace;
840+
var currentStateSent = $location.$$stateSent && changeCounter;
Has a conversation. Original line has a conversation.
767841

768-
if (!changeCounter || oldUrl != $location.absUrl()) {
842+
if (!changeCounter || oldUrl !== $location.absUrl() ||
843+
($location.$$html5 && $sniffer.history && oldState !== $location.$$state)) {
769844
changeCounter++;
770845
$rootScope.$evalAsync(function() {
771-
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
772-
defaultPrevented) {
846+
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl,
847+
$location.$$state, oldState).defaultPrevented) {
773848
$location.$$parse(oldUrl);
849+
$location.$$state = oldState;
774850
} else {
775-
$browser.url($location.absUrl(), currentReplace);
776-
afterLocationChange(oldUrl);
851+
setBrowserUrlWithFallback($location.absUrl(), currentReplace,
852+
currentStateSent ? null : $location.$$state);
853+
afterLocationChange(oldUrl, oldState);
777854
}
778855
});
779856
}
857+
$location.$$stateSent = true;
780858
$location.$$replace = false;
781859

782860
return changeCounter;
783861
});
784862

785863
return $location;
786864

787-
function afterLocationChange(oldUrl) {
788-
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
865+
function afterLocationChange(oldUrl, oldState) {
866+
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl,
867+
$location.$$state, oldState);
789868
}
790869
}];
791870
}

‎src/ngMock/angular-mocks.js

+13-3
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,10 @@ angular.mock.$Browser = function() {
4646
self.onUrlChange = function(listener) {
4747
self.pollFns.push(
4848
function() {
49-
if (self.$$lastUrl != self.$$url) {
49+
if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) {
5050
self.$$lastUrl = self.$$url;
51-
listener(self.$$url);
51+
self.$$lastState = self.$$state;
52+
listener(self.$$url, self.$$state);
5253
}
5354
}
5455
);
@@ -144,15 +145,24 @@ angular.mock.$Browser.prototype = {
144145
return pollFn;
145146
},
146147

147-
url: function(url, replace) {
148+
url: function(url, replace, state) {
149+
if (angular.isUndefined(state)) {
150+
state = null;
151+
}
148152
if (url) {
149153
this.$$url = url;
154+
// Native pushState serializes & copies the object; simulate it.
155+
this.$$state = angular.copy(state);
150156
return this;
151157
}
152158

153159
return this.$$url;
154160
},
155161

162+
state: function() {
163+
return this.$$state;
164+
},
165+
156166
cookies: function(name, value) {
157167
if (name) {
158168
if (angular.isUndefined(value)) {

‎test/ng/browserSpecs.js

+125-14
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
'use strict';
22

3+
var historyEntriesLength;
4+
var sniffer = {};
5+
36
function MockWindow() {
47
var events = {};
58
var timeouts = this.timeouts = [];
9+
var locationHref = 'http://server/';
10+
var mockWindow = this;
11+
12+
historyEntriesLength = 1;
613

714
this.setTimeout = function(fn) {
815
return timeouts.push(fn) - 1;
@@ -36,13 +43,30 @@ function MockWindow() {
3643
};
3744

3845
this.location = {
39-
href: 'http://server/',
40-
replace: noop
46+
get href() {
47+
return locationHref;
48+
},
49+
set href(value) {
50+
locationHref = value;
51+
mockWindow.history.state = null;
52+
historyEntriesLength++;
53+
},
54+
replace: function(url) {
55+
locationHref = url;
56+
mockWindow.history.state = null;
57+
},
4158
};
4259

4360
this.history = {
44-
replaceState: noop,
45-
pushState: noop
61+
state: null,
62+
pushState: function() {
63+
this.replaceState.apply(this, arguments);
64+
historyEntriesLength++;
65+
},
66+
replaceState: function(state, title, url) {
67+
locationHref = url;
68+
mockWindow.history.state = copy(state);
69+
}
4670
};
4771
}
4872

@@ -71,7 +95,7 @@ function MockDocument() {
7195

7296
describe('browser', function() {
7397
/* global Browser: false */
74-
var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts, sniffer;
98+
var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts;
7599

76100
beforeEach(function() {
77101
scripts = [];
@@ -80,9 +104,6 @@ describe('browser', function() {
80104
fakeWindow = new MockWindow();
81105
fakeDocument = new MockDocument();
82106

83-
var fakeBody = [{appendChild: function(node){scripts.push(node);},
84-
removeChild: function(node){removedScripts.push(node);}}];
85-
86107
logs = {log:[], warn:[], info:[], error:[]};
87108

88109
var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
@@ -93,6 +114,32 @@ describe('browser', function() {
93114
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
94115
});
95116

117+
describe('MockBrowser historyEntriesLength', function() {
118+
it('should increment historyEntriesLength when setting location.href', function() {
119+
expect(historyEntriesLength).toBe(1);
120+
fakeWindow.location.href = '/foo';
121+
expect(historyEntriesLength).toBe(2);
122+
});
123+
124+
it('should not increment historyEntriesLength when using location.replace', function() {
125+
expect(historyEntriesLength).toBe(1);
126+
fakeWindow.location.replace('/foo');
127+
expect(historyEntriesLength).toBe(1);
128+
});
129+
130+
it('should increment historyEntriesLength when using history.pushState', function() {
131+
expect(historyEntriesLength).toBe(1);
132+
fakeWindow.history.pushState({a: 2}, 'foo', '/bar');
133+
expect(historyEntriesLength).toBe(2);
134+
});
135+
136+
it('should not increment historyEntriesLength when using history.replaceState', function() {
137+
expect(historyEntriesLength).toBe(1);
138+
fakeWindow.history.replaceState({a: 2}, 'foo', '/bar');
139+
expect(historyEntriesLength).toBe(1);
140+
});
141+
});
142+
96143
it('should contain cookie cruncher', function() {
97144
expect(browser.cookies).toBeDefined();
98145
});
@@ -497,6 +544,68 @@ describe('browser', function() {
497544
});
498545
});
499546

547+
describe('url (when state passed)', function() {
548+
var currentHref;
549+
550+
beforeEach(function() {
551+
sniffer = {history: true, hashchange: true};
552+
currentHref = fakeWindow.location.href;
553+
});
554+
555+
it('should change state', function() {
556+
browser.url(currentHref + '/something', false, {prop: 'val1'});
557+
expect(fakeWindow.history.state).toEqual({prop: 'val1'});
558+
});
559+
560+
it('should allow to set falsy states (except `undefined`)', function() {
561+
fakeWindow.history.state = {prop: 'val1'};
562+
563+
browser.url(currentHref, false, null);
564+
expect(fakeWindow.history.state).toBe(null);
565+
566+
browser.url(currentHref, false, false);
567+
expect(fakeWindow.history.state).toBe(false);
568+
569+
browser.url(currentHref, false, '');
570+
expect(fakeWindow.history.state).toBe('');
571+
572+
browser.url(currentHref, false, 0);
573+
expect(fakeWindow.history.state).toBe(0);
574+
});
575+
576+
it('should treat `undefined` state as `null`', function() {
577+
fakeWindow.history.state = {prop: 'val1'};
578+
579+
browser.url(currentHref, false, undefined);
580+
expect(fakeWindow.history.state).toBe(null);
581+
});
582+
583+
it('should do pushState with the same URL and a different state', function() {
584+
browser.url(currentHref, false, {prop: 'val1'});
585+
expect(fakeWindow.history.state).toEqual({prop: 'val1'});
586+
587+
browser.url(currentHref, false, null);
588+
expect(fakeWindow.history.state).toBe(null);
589+
590+
browser.url(currentHref, false, {prop: 'val2'});
591+
browser.url(currentHref, false, {prop: 'val3'});
592+
expect(fakeWindow.history.state).toEqual({prop: 'val3'});
593+
});
594+
595+
it('should do pushState with the same URL and null state', function() {
596+
fakeWindow.history.state = {prop: 'val1'};
597+
browser.url(currentHref, false, null);
598+
expect(fakeWindow.history.state).toEqual(null);
599+
});
600+
601+
it('should do pushState with the same URL and the same non-null state', function() {
602+
browser.url(currentHref, false, {prop: 'val2'});
603+
fakeWindow.history.state = {prop: 'val3'};
604+
browser.url(currentHref, false, {prop: 'val2'});
605+
expect(fakeWindow.history.state).toEqual({prop: 'val2'});
606+
});
607+
});
608+
500609
describe('urlChange', function() {
501610
var callback;
502611

@@ -518,7 +627,7 @@ describe('browser', function() {
518627
fakeWindow.location.href = 'http://server/new';
519628

520629
fakeWindow.fire('popstate');
521-
expect(callback).toHaveBeenCalledWith('http://server/new');
630+
expect(callback).toHaveBeenCalledWith('http://server/new', null);
522631

523632
fakeWindow.fire('hashchange');
524633
fakeWindow.setTimeout.flush();
@@ -532,7 +641,7 @@ describe('browser', function() {
532641
fakeWindow.location.href = 'http://server/new';
533642

534643
fakeWindow.fire('popstate');
535-
expect(callback).toHaveBeenCalledWith('http://server/new');
644+
expect(callback).toHaveBeenCalledWith('http://server/new', null);
536645

537646
fakeWindow.fire('hashchange');
538647
fakeWindow.setTimeout.flush();
@@ -546,7 +655,7 @@ describe('browser', function() {
546655
fakeWindow.location.href = 'http://server/new';
547656

548657
fakeWindow.fire('hashchange');
549-
expect(callback).toHaveBeenCalledWith('http://server/new');
658+
expect(callback).toHaveBeenCalledWith('http://server/new', null);
550659

551660
fakeWindow.fire('popstate');
552661
fakeWindow.setTimeout.flush();
@@ -560,7 +669,7 @@ describe('browser', function() {
560669

561670
fakeWindow.location.href = 'http://server.new';
562671
fakeWindow.setTimeout.flush();
563-
expect(callback).toHaveBeenCalledWith('http://server.new');
672+
expect(callback).toHaveBeenCalledWith('http://server.new', null);
564673

565674
callback.reset();
566675

@@ -581,7 +690,7 @@ describe('browser', function() {
581690

582691
fakeWindow.location.href = 'http://server.new';
583692
fakeWindow.setTimeout.flush();
584-
expect(callback).toHaveBeenCalledWith('http://server.new');
693+
expect(callback).toHaveBeenCalledWith('http://server.new', null);
585694

586695
fakeWindow.fire('popstate');
587696
fakeWindow.fire('hashchange');
@@ -686,11 +795,13 @@ describe('browser', function() {
686795
var current = fakeWindow.location.href;
687796
var newUrl = 'notyet';
688797
sniffer.history = false;
798+
expect(historyEntriesLength).toBe(1);
689799
browser.url(newUrl, true);
690800
expect(browser.url()).toBe(newUrl);
801+
expect(historyEntriesLength).toBe(1);
691802
$rootScope.$digest();
692803
expect(browser.url()).toBe(newUrl);
693-
expect(fakeWindow.location.href).toBe(current);
804+
expect(historyEntriesLength).toBe(1);
694805
});
695806
});
696807

‎test/ng/locationSpec.js

+167-1
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,36 @@ describe('$location', function() {
388388
});
389389

390390

391+
describe('state', function () {
392+
it('should set $$state and return itself', function() {
393+
expect(url.$$state).toEqual(null);
394+
395+
var returned = url.state({a: 2});
396+
expect(url.$$state).toEqual({a: 2});
397+
expect(returned).toBe(url);
398+
});
399+
400+
it('should set state', function () {
401+
url.state({a: 2});
402+
expect(url.state()).toEqual({a: 2});
403+
});
404+
405+
it('should allow to set both URL and state', function() {
406+
url.url('/foo').state({a: 2});
407+
expect(url.url()).toEqual('/foo');
408+
expect(url.state()).toEqual({a: 2});
409+
});
410+
411+
it('should allow to mix state and various URL functions', function() {
412+
url.path('/foo').hash('abcd').state({a: 2}).search('bar', 'baz');
413+
expect(url.path()).toEqual('/foo');
414+
expect(url.state()).toEqual({a: 2});
415+
expect(url.search() && url.search().bar).toBe('baz');
416+
expect(url.hash()).toEqual('abcd');
417+
});
418+
});
419+
420+
391421
describe('encoding', function() {
392422

393423
it('should encode special characters', function() {
@@ -684,7 +714,7 @@ describe('$location', function() {
684714
$rootScope.$apply();
685715

686716
expect($browserUrl).toHaveBeenCalledOnce();
687-
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true]);
717+
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true, null]);
688718
expect($location.$$replace).toBe(false);
689719
}));
690720

@@ -721,6 +751,122 @@ describe('$location', function() {
721751
}));
722752
});
723753

754+
describe('wiring in html5 mode', function() {
755+
756+
beforeEach(initService({html5Mode: true, supportHistory: true}));
757+
beforeEach(inject(initBrowser({url:'http://new.com/a/b/', basePath: '/a/b/'})));
758+
759+
it('should initialize state to $browser.state()', inject(function($browser) {
760+
$browser.$$state = {a: 2};
761+
inject(function($location) {
762+
expect($location.state()).toEqual({a: 2});
763+
});
764+
}));
765+
766+
it('should update $location when browser state changes', inject(function($browser, $location) {
767+
$browser.url('http://new.com/a/b/', false, {b: 3});
768+
$browser.poll();
769+
expect($location.state()).toEqual({b: 3});
770+
}));
771+
772+
it('should replace browser url & state when replace() was called at least once',
773+
inject(function($rootScope, $location, $browser) {
774+
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
775+
$location.path('/n/url').state({a: 2}).replace();
776+
$rootScope.$apply();
777+
778+
expect($browserUrl).toHaveBeenCalledOnce();
779+
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/n/url', true, {a: 2}]);
780+
expect($location.$$replace).toBe(false);
781+
expect($location.$$state).toEqual({a: 2});
782+
}));
783+
784+
it('should use only the most recent url & state definition',
785+
inject(function($rootScope, $location, $browser) {
786+
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
787+
$location.path('/n/url').state({a: 2}).replace().state({b: 3}).path('/o/url');
788+
$rootScope.$apply();
789+
790+
expect($browserUrl).toHaveBeenCalledOnce();
791+
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/o/url', true, {b: 3}]);
792+
expect($location.$$replace).toBe(false);
793+
expect($location.$$state).toEqual({b: 3});
794+
}));
795+
796+
it('should allow to set state without touching the URL',
797+
inject(function($rootScope, $location, $browser) {
798+
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
799+
$location.state({a: 2}).replace().state({b: 3});
800+
$rootScope.$apply();
801+
802+
expect($browserUrl).toHaveBeenCalledOnce();
803+
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/', true, {b: 3}]);
804+
expect($location.$$replace).toBe(false);
805+
expect($location.$$state).toEqual({b: 3});
806+
}));
807+
808+
it('should always reset replace flag after running watch', inject(function($rootScope, $location) {
809+
// init watches
810+
$location.url('/initUrl').state({a: 2});
811+
$rootScope.$apply();
812+
813+
// changes url & state but resets them before digest
814+
$location.url('/newUrl').state({a: 2}).replace().state({b: 3}).url('/initUrl');
815+
$rootScope.$apply();
816+
expect($location.$$replace).toBe(false);
817+
818+
// set the url to the old value
819+
$location.url('/newUrl').state({a: 2}).replace();
820+
$rootScope.$apply();
821+
expect($location.$$replace).toBe(false);
822+
823+
// doesn't even change url only calls replace()
824+
$location.replace();
825+
$rootScope.$apply();
826+
expect($location.$$replace).toBe(false);
827+
}));
828+
829+
it('should allow to modify state only before digest',
830+
inject(function($rootScope, $location, $browser) {
831+
var o = {a: 2};
832+
$location.state(o);
833+
o.a = 3;
834+
$rootScope.$apply();
835+
expect($browser.state()).toEqual({a: 3});
836+
837+
o.a = 4;
838+
$rootScope.$apply();
839+
expect($browser.state()).toEqual({a: 3});
840+
}));
841+
842+
it('should make $location.state() referencially identical with $browser.state() after digest',
843+
inject(function($rootScope, $location, $browser) {
844+
$location.state({a: 2});
845+
$rootScope.$apply();
846+
expect($location.state()).toBe($browser.state());
847+
}));
848+
849+
it('should allow to query the state after digest',
850+
inject(function($rootScope, $location) {
851+
$location.url('/foo').state({a: 2});
852+
$rootScope.$apply();
853+
expect($location.state()).toEqual({a: 2});
854+
}));
855+
856+
it('should reset the state on .url() after digest',
857+
inject(function($rootScope, $location, $browser) {
858+
$location.url('/foo').state({a: 2});
859+
$rootScope.$apply();
860+
861+
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
862+
$location.url('/bar');
863+
$rootScope.$apply();
864+
865+
expect($browserUrl).toHaveBeenCalledOnce();
866+
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/bar', false, null]);
867+
}));
868+
});
869+
724870

725871
// html5 history is disabled
726872
describe('disabled history', function() {
@@ -1771,9 +1917,21 @@ describe('$location', function() {
17711917
"$location in HTML5 mode requires a <base> tag to be present!");
17721918
});
17731919
});
1920+
1921+
it('should support state', function() {
1922+
expect(location.state({a: 2}).state()).toEqual({a: 2});
1923+
});
17741924
});
17751925

17761926

1927+
function throwOnState(location) {
1928+
expect(function () {
1929+
location.state({a: 2});
1930+
}).toThrowMinErr('$location', 'nostate', 'History API state support is available only ' +
1931+
'in HTML5 mode and only in browsers supporting HTML5 History API'
1932+
);
1933+
}
1934+
17771935
describe('LocationHashbangUrl', function() {
17781936
var location;
17791937

@@ -1828,6 +1986,10 @@ describe('$location', function() {
18281986
expect(location.url()).toBe('/http://example.com/');
18291987
expect(location.absUrl()).toBe('http://server/pre/index.html#/http://example.com/');
18301988
});
1989+
1990+
it('should throw on url(urlString, stateObject)', function () {
1991+
throwOnState(location);
1992+
});
18311993
});
18321994

18331995

@@ -1854,5 +2016,9 @@ describe('$location', function() {
18542016
// Note: relies on the previous state!
18552017
expect(parseLinkAndReturn(locationIndex, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/index.html#!/otherPath#test');
18562018
});
2019+
2020+
it('should throw on url(urlString, stateObject)', function () {
2021+
throwOnState(location);
2022+
});
18572023
});
18582024
});

‎test/ng/rafSpec.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ describe('$$rAF', function() {
7979
//we need to create our own injector to work around the ngMock overrides
8080
var injector = createInjector(['ng', function($provide) {
8181
$provide.value('$window', {
82-
location : window.location,
82+
location: window.location,
83+
history: window.history,
8384
webkitRequestAnimationFrame: jasmine.createSpy('$window.webkitRequestAnimationFrame'),
8485
webkitCancelRequestAnimationFrame: jasmine.createSpy('$window.webkitCancelRequestAnimationFrame')
8586
});

‎test/ngRoute/routeSpec.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -883,7 +883,7 @@ describe('$route', function() {
883883

884884
expect($location.path()).toEqual('/bar/id3');
885885
expect($browserUrl.mostRecentCall.args)
886-
.toEqual(['http://server/#/bar/id3?extra=eId', true]);
886+
.toEqual(['http://server/#/bar/id3?extra=eId', true, null]);
887887
});
888888
});
889889
});

0 commit comments

Comments
 (0)
This repository has been archived.