mirror of
https://github.com/zhigang1992/angular.js.git
synced 2026-04-17 22:34:43 +08:00
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
This commit is contained in:
committed by
Igor Minar
parent
8ee1ba4b94
commit
6fd36deed9
8
docs/content/error/$location/nostate.ngdoc
Normal file
8
docs/content/error/$location/nostate.ngdoc
Normal file
@@ -0,0 +1,8 @@
|
||||
@ngdoc error
|
||||
@name $location:nostate
|
||||
@fullName History API state support is available only in HTML5 mode and only in browsers supporting HTML5 History API
|
||||
@description
|
||||
|
||||
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).
|
||||
|
||||
To avoid this error, either drop support for those older browsers or avoid using this method.
|
||||
@@ -124,6 +124,7 @@ function Browser(window, document, $log, $sniffer) {
|
||||
//////////////////////////////////////////////////////////////
|
||||
|
||||
var lastBrowserUrl = location.href,
|
||||
lastHistoryState = history.state,
|
||||
baseElement = document.find('base'),
|
||||
reloadLocation = null;
|
||||
|
||||
@@ -144,27 +145,38 @@ function Browser(window, document, $log, $sniffer) {
|
||||
* {@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 {boolean=} replace Should new url replace current history record?
|
||||
* @param {object=} state object to use with pushState/replaceState
|
||||
*/
|
||||
self.url = function(url, replace) {
|
||||
self.url = function(url, replace, state) {
|
||||
// In modern browsers `history.state` is `null` by default; treating it separately
|
||||
// from `undefined` would cause `$browser.url('/foo')` to change `history.state`
|
||||
// to undefined via `pushState`. Instead, let's change `undefined` to `null` here.
|
||||
if (isUndefined(state)) {
|
||||
state = null;
|
||||
}
|
||||
|
||||
// Android Browser BFCache causes location, history reference to become stale.
|
||||
if (location !== window.location) location = window.location;
|
||||
if (history !== window.history) history = window.history;
|
||||
|
||||
// setter
|
||||
if (url) {
|
||||
if (lastBrowserUrl == url) return;
|
||||
// Don't change anything if previous and current URLs and states match. This also prevents
|
||||
// IE<10 from getting into redirect loop when in LocationHashbangInHtml5Url mode.
|
||||
// See https://github.com/angular/angular.js/commit/ffb2701
|
||||
if (lastBrowserUrl === url && (!$sniffer.history || history.state === state)) {
|
||||
return;
|
||||
}
|
||||
var sameBase = lastBrowserUrl && stripHash(lastBrowserUrl) === stripHash(url);
|
||||
lastBrowserUrl = url;
|
||||
// Don't use history API if only the hash changed
|
||||
// due to a bug in IE10/IE11 which leads
|
||||
// to not firing a `hashchange` nor `popstate` event
|
||||
// in some cases (see #9143).
|
||||
if (!sameBase && $sniffer.history) {
|
||||
if (replace) history.replaceState(null, '', url);
|
||||
else {
|
||||
history.pushState(null, '', url);
|
||||
}
|
||||
if ($sniffer.history && (!sameBase || history.state !== state)) {
|
||||
history[replace ? 'replaceState' : 'pushState'](state, '', url);
|
||||
lastHistoryState = history.state;
|
||||
} else {
|
||||
if (!sameBase) {
|
||||
reloadLocation = url;
|
||||
@@ -185,15 +197,31 @@ function Browser(window, document, $log, $sniffer) {
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @name $browser#state
|
||||
*
|
||||
* @description
|
||||
* This method is a getter.
|
||||
*
|
||||
* Return history.state or null if history.state is undefined.
|
||||
*
|
||||
* @returns {object} state
|
||||
*/
|
||||
self.state = function() {
|
||||
return isUndefined(history.state) ? null : history.state;
|
||||
};
|
||||
|
||||
var urlChangeListeners = [],
|
||||
urlChangeInit = false;
|
||||
|
||||
function fireUrlChange() {
|
||||
if (lastBrowserUrl == self.url()) return;
|
||||
if (lastBrowserUrl === self.url() && lastHistoryState === history.state) {
|
||||
return;
|
||||
}
|
||||
|
||||
lastBrowserUrl = self.url();
|
||||
forEach(urlChangeListeners, function(listener) {
|
||||
listener(self.url());
|
||||
listener(self.url(), history.state);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -303,9 +303,7 @@ function LocationHashbangInHtml5Url(appBase, hashPrefix) {
|
||||
}
|
||||
|
||||
|
||||
LocationHashbangInHtml5Url.prototype =
|
||||
LocationHashbangUrl.prototype =
|
||||
LocationHtml5Url.prototype = {
|
||||
var locationPrototype = {
|
||||
|
||||
/**
|
||||
* Are we in html5 mode?
|
||||
@@ -314,7 +312,7 @@ LocationHashbangInHtml5Url.prototype =
|
||||
$$html5: false,
|
||||
|
||||
/**
|
||||
* Has any change been replacing ?
|
||||
* Has any change been replacing?
|
||||
* @private
|
||||
*/
|
||||
$$replace: false,
|
||||
@@ -530,6 +528,46 @@ LocationHashbangInHtml5Url.prototype =
|
||||
}
|
||||
};
|
||||
|
||||
forEach([LocationHashbangInHtml5Url, LocationHashbangUrl, LocationHtml5Url], function (Location) {
|
||||
Location.prototype = Object.create(locationPrototype);
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $location#state
|
||||
*
|
||||
* @description
|
||||
* This method is getter / setter.
|
||||
*
|
||||
* Return the history state object when called without any parameter.
|
||||
*
|
||||
* Change the history state object when called with one parameter and return `$location`.
|
||||
* The state object is later passed to `pushState` or `replaceState`.
|
||||
*
|
||||
* NOTE: This method is supported only in HTML5 mode and only in browsers supporting
|
||||
* the HTML5 History API (i.e. methods `pushState` and `replaceState`). If you need to support
|
||||
* older browsers (like IE9 or Android < 4.0), don't use this method.
|
||||
*
|
||||
* @param {object=} state State object for pushState or replaceState
|
||||
* @return {object} state
|
||||
*/
|
||||
Location.prototype.state = function(state) {
|
||||
if (!arguments.length)
|
||||
return this.$$state;
|
||||
|
||||
if (Location !== LocationHtml5Url || !this.$$html5) {
|
||||
throw $locationMinErr('nostate', 'History API state support is available only ' +
|
||||
'in HTML5 mode and only in browsers supporting HTML5 History API');
|
||||
}
|
||||
// The user might modify `stateObject` after invoking `$location.state(stateObject)`
|
||||
// but we're changing the $$state reference to $browser.state() during the $digest
|
||||
// so the modification window is narrow.
|
||||
this.$$state = isUndefined(state) ? null : state;
|
||||
|
||||
return this;
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
function locationGetter(property) {
|
||||
return function() {
|
||||
return this[property];
|
||||
@@ -649,9 +687,14 @@ function $LocationProvider(){
|
||||
* details about event object. Upon successful change
|
||||
* {@link ng.$location#events_$locationChangeSuccess $locationChangeSuccess} is fired.
|
||||
*
|
||||
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
|
||||
* the browser supports the HTML5 History API.
|
||||
*
|
||||
* @param {Object} angularEvent Synthetic event object.
|
||||
* @param {string} newUrl New URL
|
||||
* @param {string=} oldUrl URL that was before it was changed.
|
||||
* @param {string=} newState New history state object
|
||||
* @param {string=} oldState History state object that was before it was changed.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -661,9 +704,14 @@ function $LocationProvider(){
|
||||
* @description
|
||||
* Broadcasted after a URL was changed.
|
||||
*
|
||||
* The `newState` and `oldState` parameters may be defined only in HTML5 mode and when
|
||||
* the browser supports the HTML5 History API.
|
||||
*
|
||||
* @param {Object} angularEvent Synthetic event object.
|
||||
* @param {string} newUrl New URL
|
||||
* @param {string=} oldUrl URL that was before it was changed.
|
||||
* @param {string=} newState New history state object
|
||||
* @param {string=} oldState History state object that was before it was changed.
|
||||
*/
|
||||
|
||||
this.$get = ['$rootScope', '$browser', '$sniffer', '$rootElement',
|
||||
@@ -688,8 +736,29 @@ function $LocationProvider(){
|
||||
$location = new LocationMode(appBase, '#' + hashPrefix);
|
||||
$location.$$parseLinkUrl(initialUrl, initialUrl);
|
||||
|
||||
$location.$$state = $browser.state();
|
||||
|
||||
var IGNORE_URI_REGEXP = /^\s*(javascript|mailto):/i;
|
||||
|
||||
function setBrowserUrlWithFallback(url, replace, state) {
|
||||
var oldUrl = $location.url();
|
||||
var oldState = $location.$$state;
|
||||
try {
|
||||
$browser.url(url, replace, state);
|
||||
|
||||
// Make sure $location.state() returns referentially identical (not just deeply equal)
|
||||
// state object; this makes possible quick checking if the state changed in the digest
|
||||
// loop. Checking deep equality would be too expensive.
|
||||
$location.$$state = $browser.state();
|
||||
} catch (e) {
|
||||
// Restore old values if pushState fails
|
||||
$location.url(oldUrl);
|
||||
$location.$$state = oldState;
|
||||
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
$rootElement.on('click', function(event) {
|
||||
// TODO(vojta): rewrite link when opening in new tab/window (in legacy browser)
|
||||
// currently we open nice url link and redirect then
|
||||
@@ -740,52 +809,63 @@ function $LocationProvider(){
|
||||
$browser.url($location.absUrl(), true);
|
||||
}
|
||||
|
||||
// update $location when $browser url changes
|
||||
$browser.onUrlChange(function(newUrl) {
|
||||
if ($location.absUrl() != newUrl) {
|
||||
$rootScope.$evalAsync(function() {
|
||||
var oldUrl = $location.absUrl();
|
||||
var initializing = true;
|
||||
|
||||
$location.$$parse(newUrl);
|
||||
if ($rootScope.$broadcast('$locationChangeStart', newUrl,
|
||||
oldUrl).defaultPrevented) {
|
||||
$location.$$parse(oldUrl);
|
||||
$browser.url(oldUrl);
|
||||
} else {
|
||||
afterLocationChange(oldUrl);
|
||||
}
|
||||
});
|
||||
if (!$rootScope.$$phase) $rootScope.$digest();
|
||||
}
|
||||
// update $location when $browser url changes
|
||||
$browser.onUrlChange(function(newUrl, newState) {
|
||||
$rootScope.$evalAsync(function() {
|
||||
var oldUrl = $location.absUrl();
|
||||
var oldState = $location.$$state;
|
||||
|
||||
$location.$$parse(newUrl);
|
||||
$location.$$state = newState;
|
||||
if ($rootScope.$broadcast('$locationChangeStart', newUrl, oldUrl,
|
||||
newState, oldState).defaultPrevented) {
|
||||
$location.$$parse(oldUrl);
|
||||
$location.$$state = oldState;
|
||||
setBrowserUrlWithFallback(oldUrl, false, oldState);
|
||||
} else {
|
||||
initializing = false;
|
||||
afterLocationChange(oldUrl, oldState);
|
||||
}
|
||||
});
|
||||
if (!$rootScope.$$phase) $rootScope.$digest();
|
||||
});
|
||||
|
||||
// update browser
|
||||
var changeCounter = 0;
|
||||
$rootScope.$watch(function $locationWatch() {
|
||||
var oldUrl = $browser.url();
|
||||
var oldState = $browser.state();
|
||||
var currentReplace = $location.$$replace;
|
||||
|
||||
if (!changeCounter || oldUrl != $location.absUrl()) {
|
||||
changeCounter++;
|
||||
if (initializing || oldUrl !== $location.absUrl() ||
|
||||
($location.$$html5 && $sniffer.history && oldState !== $location.$$state)) {
|
||||
initializing = false;
|
||||
|
||||
$rootScope.$evalAsync(function() {
|
||||
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl).
|
||||
defaultPrevented) {
|
||||
if ($rootScope.$broadcast('$locationChangeStart', $location.absUrl(), oldUrl,
|
||||
$location.$$state, oldState).defaultPrevented) {
|
||||
$location.$$parse(oldUrl);
|
||||
$location.$$state = oldState;
|
||||
} else {
|
||||
$browser.url($location.absUrl(), currentReplace);
|
||||
afterLocationChange(oldUrl);
|
||||
setBrowserUrlWithFallback($location.absUrl(), currentReplace,
|
||||
oldState === $location.$$state ? null : $location.$$state);
|
||||
afterLocationChange(oldUrl, oldState);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
$location.$$replace = false;
|
||||
|
||||
return changeCounter;
|
||||
// we don't need to return anything because $evalAsync will make the digest loop dirty when
|
||||
// there is a change
|
||||
});
|
||||
|
||||
return $location;
|
||||
|
||||
function afterLocationChange(oldUrl) {
|
||||
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl);
|
||||
function afterLocationChange(oldUrl, oldState) {
|
||||
$rootScope.$broadcast('$locationChangeSuccess', $location.absUrl(), oldUrl,
|
||||
$location.$$state, oldState);
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
16
src/ngMock/angular-mocks.js
vendored
16
src/ngMock/angular-mocks.js
vendored
@@ -46,9 +46,10 @@ angular.mock.$Browser = function() {
|
||||
self.onUrlChange = function(listener) {
|
||||
self.pollFns.push(
|
||||
function() {
|
||||
if (self.$$lastUrl != self.$$url) {
|
||||
if (self.$$lastUrl !== self.$$url || self.$$state !== self.$$lastState) {
|
||||
self.$$lastUrl = self.$$url;
|
||||
listener(self.$$url);
|
||||
self.$$lastState = self.$$state;
|
||||
listener(self.$$url, self.$$state);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -144,15 +145,24 @@ angular.mock.$Browser.prototype = {
|
||||
return pollFn;
|
||||
},
|
||||
|
||||
url: function(url, replace) {
|
||||
url: function(url, replace, state) {
|
||||
if (angular.isUndefined(state)) {
|
||||
state = null;
|
||||
}
|
||||
if (url) {
|
||||
this.$$url = url;
|
||||
// Native pushState serializes & copies the object; simulate it.
|
||||
this.$$state = angular.copy(state);
|
||||
return this;
|
||||
}
|
||||
|
||||
return this.$$url;
|
||||
},
|
||||
|
||||
state: function() {
|
||||
return this.$$state;
|
||||
},
|
||||
|
||||
cookies: function(name, value) {
|
||||
if (name) {
|
||||
if (angular.isUndefined(value)) {
|
||||
|
||||
@@ -1,8 +1,15 @@
|
||||
'use strict';
|
||||
|
||||
var historyEntriesLength;
|
||||
var sniffer = {};
|
||||
|
||||
function MockWindow() {
|
||||
var events = {};
|
||||
var timeouts = this.timeouts = [];
|
||||
var locationHref = 'http://server/';
|
||||
var mockWindow = this;
|
||||
|
||||
historyEntriesLength = 1;
|
||||
|
||||
this.setTimeout = function(fn) {
|
||||
return timeouts.push(fn) - 1;
|
||||
@@ -36,13 +43,30 @@ function MockWindow() {
|
||||
};
|
||||
|
||||
this.location = {
|
||||
href: 'http://server/',
|
||||
replace: noop
|
||||
get href() {
|
||||
return locationHref;
|
||||
},
|
||||
set href(value) {
|
||||
locationHref = value;
|
||||
mockWindow.history.state = null;
|
||||
historyEntriesLength++;
|
||||
},
|
||||
replace: function(url) {
|
||||
locationHref = url;
|
||||
mockWindow.history.state = null;
|
||||
},
|
||||
};
|
||||
|
||||
this.history = {
|
||||
replaceState: noop,
|
||||
pushState: noop
|
||||
state: null,
|
||||
pushState: function() {
|
||||
this.replaceState.apply(this, arguments);
|
||||
historyEntriesLength++;
|
||||
},
|
||||
replaceState: function(state, title, url) {
|
||||
locationHref = url;
|
||||
mockWindow.history.state = copy(state);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -71,7 +95,7 @@ function MockDocument() {
|
||||
|
||||
describe('browser', function() {
|
||||
/* global Browser: false */
|
||||
var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts, sniffer;
|
||||
var browser, fakeWindow, fakeDocument, logs, scripts, removedScripts;
|
||||
|
||||
beforeEach(function() {
|
||||
scripts = [];
|
||||
@@ -80,9 +104,6 @@ describe('browser', function() {
|
||||
fakeWindow = new MockWindow();
|
||||
fakeDocument = new MockDocument();
|
||||
|
||||
var fakeBody = [{appendChild: function(node){scripts.push(node);},
|
||||
removeChild: function(node){removedScripts.push(node);}}];
|
||||
|
||||
logs = {log:[], warn:[], info:[], error:[]};
|
||||
|
||||
var fakeLog = {log: function() { logs.log.push(slice.call(arguments)); },
|
||||
@@ -93,6 +114,32 @@ describe('browser', function() {
|
||||
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer);
|
||||
});
|
||||
|
||||
describe('MockBrowser historyEntriesLength', function() {
|
||||
it('should increment historyEntriesLength when setting location.href', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.location.href = '/foo';
|
||||
expect(historyEntriesLength).toBe(2);
|
||||
});
|
||||
|
||||
it('should not increment historyEntriesLength when using location.replace', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.location.replace('/foo');
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
});
|
||||
|
||||
it('should increment historyEntriesLength when using history.pushState', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.history.pushState({a: 2}, 'foo', '/bar');
|
||||
expect(historyEntriesLength).toBe(2);
|
||||
});
|
||||
|
||||
it('should not increment historyEntriesLength when using history.replaceState', function() {
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
fakeWindow.history.replaceState({a: 2}, 'foo', '/bar');
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain cookie cruncher', function() {
|
||||
expect(browser.cookies).toBeDefined();
|
||||
});
|
||||
@@ -546,6 +593,68 @@ describe('browser', function() {
|
||||
|
||||
});
|
||||
|
||||
describe('url (when state passed)', function() {
|
||||
var currentHref;
|
||||
|
||||
beforeEach(function() {
|
||||
sniffer = {history: true, hashchange: true};
|
||||
currentHref = fakeWindow.location.href;
|
||||
});
|
||||
|
||||
it('should change state', function() {
|
||||
browser.url(currentHref + '/something', false, {prop: 'val1'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val1'});
|
||||
});
|
||||
|
||||
it('should allow to set falsy states (except `undefined`)', function() {
|
||||
fakeWindow.history.state = {prop: 'val1'};
|
||||
|
||||
browser.url(currentHref, false, null);
|
||||
expect(fakeWindow.history.state).toBe(null);
|
||||
|
||||
browser.url(currentHref, false, false);
|
||||
expect(fakeWindow.history.state).toBe(false);
|
||||
|
||||
browser.url(currentHref, false, '');
|
||||
expect(fakeWindow.history.state).toBe('');
|
||||
|
||||
browser.url(currentHref, false, 0);
|
||||
expect(fakeWindow.history.state).toBe(0);
|
||||
});
|
||||
|
||||
it('should treat `undefined` state as `null`', function() {
|
||||
fakeWindow.history.state = {prop: 'val1'};
|
||||
|
||||
browser.url(currentHref, false, undefined);
|
||||
expect(fakeWindow.history.state).toBe(null);
|
||||
});
|
||||
|
||||
it('should do pushState with the same URL and a different state', function() {
|
||||
browser.url(currentHref, false, {prop: 'val1'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val1'});
|
||||
|
||||
browser.url(currentHref, false, null);
|
||||
expect(fakeWindow.history.state).toBe(null);
|
||||
|
||||
browser.url(currentHref, false, {prop: 'val2'});
|
||||
browser.url(currentHref, false, {prop: 'val3'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val3'});
|
||||
});
|
||||
|
||||
it('should do pushState with the same URL and null state', function() {
|
||||
fakeWindow.history.state = {prop: 'val1'};
|
||||
browser.url(currentHref, false, null);
|
||||
expect(fakeWindow.history.state).toEqual(null);
|
||||
});
|
||||
|
||||
it('should do pushState with the same URL and the same non-null state', function() {
|
||||
browser.url(currentHref, false, {prop: 'val2'});
|
||||
fakeWindow.history.state = {prop: 'val3'};
|
||||
browser.url(currentHref, false, {prop: 'val2'});
|
||||
expect(fakeWindow.history.state).toEqual({prop: 'val2'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('urlChange', function() {
|
||||
var callback;
|
||||
|
||||
@@ -567,7 +676,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/new';
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new', null);
|
||||
|
||||
fakeWindow.fire('hashchange');
|
||||
fakeWindow.setTimeout.flush();
|
||||
@@ -581,7 +690,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/new';
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new', null);
|
||||
|
||||
fakeWindow.fire('hashchange');
|
||||
fakeWindow.setTimeout.flush();
|
||||
@@ -595,7 +704,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/new';
|
||||
|
||||
fakeWindow.fire('hashchange');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/new', null);
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
fakeWindow.setTimeout.flush();
|
||||
@@ -609,7 +718,7 @@ describe('browser', function() {
|
||||
|
||||
fakeWindow.location.href = 'http://server.new';
|
||||
fakeWindow.setTimeout.flush();
|
||||
expect(callback).toHaveBeenCalledWith('http://server.new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server.new', null);
|
||||
|
||||
callback.reset();
|
||||
|
||||
@@ -630,7 +739,7 @@ describe('browser', function() {
|
||||
|
||||
fakeWindow.location.href = 'http://server/#new';
|
||||
fakeWindow.setTimeout.flush();
|
||||
expect(callback).toHaveBeenCalledWith('http://server/#new');
|
||||
expect(callback).toHaveBeenCalledWith('http://server/#new', null);
|
||||
|
||||
fakeWindow.fire('popstate');
|
||||
fakeWindow.fire('hashchange');
|
||||
@@ -801,7 +910,7 @@ describe('browser', function() {
|
||||
fakeWindow.location.href = 'http://server/some/deep/path';
|
||||
var changeUrlCount = 0;
|
||||
var _url = browser.url;
|
||||
browser.url = function(newUrl, replace) {
|
||||
browser.url = function(newUrl, replace, state) {
|
||||
if (newUrl) {
|
||||
changeUrlCount++;
|
||||
}
|
||||
@@ -817,7 +926,7 @@ describe('browser', function() {
|
||||
// from $location for rewriting the initial url into a hash url
|
||||
expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', true);
|
||||
// from the initial call to the watch in $location for watching $location
|
||||
expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', false);
|
||||
expect(browser.url).toHaveBeenCalledWith('http://server/#/some/deep/path', false, null);
|
||||
expect(changeUrlCount).toBe(2);
|
||||
});
|
||||
|
||||
@@ -836,11 +945,13 @@ describe('browser', function() {
|
||||
var current = fakeWindow.location.href;
|
||||
var newUrl = 'notyet';
|
||||
sniffer.history = false;
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
browser.url(newUrl, true);
|
||||
expect(browser.url()).toBe(newUrl);
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
$rootScope.$digest();
|
||||
expect(browser.url()).toBe(newUrl);
|
||||
expect(fakeWindow.location.href).toBe(current);
|
||||
expect(historyEntriesLength).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -388,6 +388,36 @@ describe('$location', function() {
|
||||
});
|
||||
|
||||
|
||||
describe('state', function () {
|
||||
it('should set $$state and return itself', function() {
|
||||
expect(url.$$state).toEqual(null);
|
||||
|
||||
var returned = url.state({a: 2});
|
||||
expect(url.$$state).toEqual({a: 2});
|
||||
expect(returned).toBe(url);
|
||||
});
|
||||
|
||||
it('should set state', function () {
|
||||
url.state({a: 2});
|
||||
expect(url.state()).toEqual({a: 2});
|
||||
});
|
||||
|
||||
it('should allow to set both URL and state', function() {
|
||||
url.url('/foo').state({a: 2});
|
||||
expect(url.url()).toEqual('/foo');
|
||||
expect(url.state()).toEqual({a: 2});
|
||||
});
|
||||
|
||||
it('should allow to mix state and various URL functions', function() {
|
||||
url.path('/foo').hash('abcd').state({a: 2}).search('bar', 'baz');
|
||||
expect(url.path()).toEqual('/foo');
|
||||
expect(url.state()).toEqual({a: 2});
|
||||
expect(url.search() && url.search().bar).toBe('baz');
|
||||
expect(url.hash()).toEqual('abcd');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('encoding', function() {
|
||||
|
||||
it('should encode special characters', function() {
|
||||
@@ -684,7 +714,7 @@ describe('$location', function() {
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true]);
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b#!/n/url', true, null]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
}));
|
||||
|
||||
@@ -721,6 +751,122 @@ describe('$location', function() {
|
||||
}));
|
||||
});
|
||||
|
||||
describe('wiring in html5 mode', function() {
|
||||
|
||||
beforeEach(initService({html5Mode: true, supportHistory: true}));
|
||||
beforeEach(inject(initBrowser({url:'http://new.com/a/b/', basePath: '/a/b/'})));
|
||||
|
||||
it('should initialize state to $browser.state()', inject(function($browser) {
|
||||
$browser.$$state = {a: 2};
|
||||
inject(function($location) {
|
||||
expect($location.state()).toEqual({a: 2});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should update $location when browser state changes', inject(function($browser, $location) {
|
||||
$browser.url('http://new.com/a/b/', false, {b: 3});
|
||||
$browser.poll();
|
||||
expect($location.state()).toEqual({b: 3});
|
||||
}));
|
||||
|
||||
it('should replace browser url & state when replace() was called at least once',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.path('/n/url').state({a: 2}).replace();
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/n/url', true, {a: 2}]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
expect($location.$$state).toEqual({a: 2});
|
||||
}));
|
||||
|
||||
it('should use only the most recent url & state definition',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.path('/n/url').state({a: 2}).replace().state({b: 3}).path('/o/url');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/o/url', true, {b: 3}]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
expect($location.$$state).toEqual({b: 3});
|
||||
}));
|
||||
|
||||
it('should allow to set state without touching the URL',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.state({a: 2}).replace().state({b: 3});
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/', true, {b: 3}]);
|
||||
expect($location.$$replace).toBe(false);
|
||||
expect($location.$$state).toEqual({b: 3});
|
||||
}));
|
||||
|
||||
it('should always reset replace flag after running watch', inject(function($rootScope, $location) {
|
||||
// init watches
|
||||
$location.url('/initUrl').state({a: 2});
|
||||
$rootScope.$apply();
|
||||
|
||||
// changes url & state but resets them before digest
|
||||
$location.url('/newUrl').state({a: 2}).replace().state({b: 3}).url('/initUrl');
|
||||
$rootScope.$apply();
|
||||
expect($location.$$replace).toBe(false);
|
||||
|
||||
// set the url to the old value
|
||||
$location.url('/newUrl').state({a: 2}).replace();
|
||||
$rootScope.$apply();
|
||||
expect($location.$$replace).toBe(false);
|
||||
|
||||
// doesn't even change url only calls replace()
|
||||
$location.replace();
|
||||
$rootScope.$apply();
|
||||
expect($location.$$replace).toBe(false);
|
||||
}));
|
||||
|
||||
it('should allow to modify state only before digest',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
var o = {a: 2};
|
||||
$location.state(o);
|
||||
o.a = 3;
|
||||
$rootScope.$apply();
|
||||
expect($browser.state()).toEqual({a: 3});
|
||||
|
||||
o.a = 4;
|
||||
$rootScope.$apply();
|
||||
expect($browser.state()).toEqual({a: 3});
|
||||
}));
|
||||
|
||||
it('should make $location.state() referencially identical with $browser.state() after digest',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
$location.state({a: 2});
|
||||
$rootScope.$apply();
|
||||
expect($location.state()).toBe($browser.state());
|
||||
}));
|
||||
|
||||
it('should allow to query the state after digest',
|
||||
inject(function($rootScope, $location) {
|
||||
$location.url('/foo').state({a: 2});
|
||||
$rootScope.$apply();
|
||||
expect($location.state()).toEqual({a: 2});
|
||||
}));
|
||||
|
||||
it('should reset the state on .url() after digest',
|
||||
inject(function($rootScope, $location, $browser) {
|
||||
$location.url('/foo').state({a: 2});
|
||||
$rootScope.$apply();
|
||||
|
||||
var $browserUrl = spyOnlyCallsWithArgs($browser, 'url').andCallThrough();
|
||||
$location.url('/bar');
|
||||
$rootScope.$apply();
|
||||
|
||||
expect($browserUrl).toHaveBeenCalledOnce();
|
||||
expect($browserUrl.mostRecentCall.args).toEqual(['http://new.com/a/b/bar', false, null]);
|
||||
}));
|
||||
});
|
||||
|
||||
|
||||
// html5 history is disabled
|
||||
describe('disabled history', function() {
|
||||
@@ -1771,9 +1917,21 @@ describe('$location', function() {
|
||||
"$location in HTML5 mode requires a <base> tag to be present!");
|
||||
});
|
||||
});
|
||||
|
||||
it('should support state', function() {
|
||||
expect(location.state({a: 2}).state()).toEqual({a: 2});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
function throwOnState(location) {
|
||||
expect(function () {
|
||||
location.state({a: 2});
|
||||
}).toThrowMinErr('$location', 'nostate', 'History API state support is available only ' +
|
||||
'in HTML5 mode and only in browsers supporting HTML5 History API'
|
||||
);
|
||||
}
|
||||
|
||||
describe('LocationHashbangUrl', function() {
|
||||
var location;
|
||||
|
||||
@@ -1828,6 +1986,10 @@ describe('$location', function() {
|
||||
expect(location.url()).toBe('/http://example.com/');
|
||||
expect(location.absUrl()).toBe('http://server/pre/index.html#/http://example.com/');
|
||||
});
|
||||
|
||||
it('should throw on url(urlString, stateObject)', function () {
|
||||
throwOnState(location);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1854,5 +2016,9 @@ describe('$location', function() {
|
||||
// Note: relies on the previous state!
|
||||
expect(parseLinkAndReturn(locationIndex, 'someIgnoredAbsoluteHref', '#test')).toEqual('http://server/pre/index.html#!/otherPath#test');
|
||||
});
|
||||
|
||||
it('should throw on url(urlString, stateObject)', function () {
|
||||
throwOnState(location);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -79,7 +79,8 @@ describe('$$rAF', function() {
|
||||
//we need to create our own injector to work around the ngMock overrides
|
||||
var injector = createInjector(['ng', function($provide) {
|
||||
$provide.value('$window', {
|
||||
location : window.location,
|
||||
location: window.location,
|
||||
history: window.history,
|
||||
webkitRequestAnimationFrame: jasmine.createSpy('$window.webkitRequestAnimationFrame'),
|
||||
webkitCancelRequestAnimationFrame: jasmine.createSpy('$window.webkitCancelRequestAnimationFrame')
|
||||
});
|
||||
|
||||
@@ -883,7 +883,7 @@ describe('$route', function() {
|
||||
|
||||
expect($location.path()).toEqual('/bar/id3');
|
||||
expect($browserUrl.mostRecentCall.args)
|
||||
.toEqual(['http://server/#/bar/id3?extra=eId', true]);
|
||||
.toEqual(['http://server/#/bar/id3?extra=eId', true, null]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user