'use strict';
/* global getHash:true, stripHash:true */
var historyEntriesLength;
var sniffer = {};
function MockWindow(options) {
if (typeof options !== 'object') {
options = {};
}
var events = {};
var timeouts = this.timeouts = [];
var locationHref = window.document.createElement('a');
var committedHref = window.document.createElement('a');
locationHref.href = committedHref.href = 'http://server/';
var mockWindow = this;
var msie = options.msie;
var ieState;
historyEntriesLength = 1;
function replaceHash(href, hash) {
// replace the hash with the new one (stripping off a leading hash if there is one)
// See hash setter spec: https://url.spec.whatwg.org/#urlutils-and-urlutilsreadonly-members
return stripHash(href) + '#' + hash.replace(/^#/,'');
}
this.setTimeout = function(fn) {
return timeouts.push(fn) - 1;
};
this.clearTimeout = function(id) {
timeouts[id] = noop;
};
this.setTimeout.flush = function(count) {
count = count || timeouts.length;
while (count-- > 0) timeouts.shift()();
};
this.addEventListener = function(name, listener) {
if (angular.isUndefined(events[name])) events[name] = [];
events[name].push(listener);
};
this.removeEventListener = noop;
this.fire = function(name) {
forEach(events[name], function(fn) {
// type/target to make jQuery happy
fn({
type: name,
target: {
nodeType: 1
}
});
});
};
this.location = {
get href() {
return committedHref.href;
},
set href(value) {
locationHref.href = value;
mockWindow.history.state = null;
historyEntriesLength++;
if (!options.updateAsync) this.flushHref();
},
get hash() {
return getHash(committedHref.href);
},
set hash(value) {
locationHref.href = replaceHash(locationHref.href, value);
if (!options.updateAsync) this.flushHref();
},
replace: function(url) {
locationHref.href = url;
mockWindow.history.state = null;
if (!options.updateAsync) this.flushHref();
},
flushHref: function() {
committedHref.href = locationHref.href;
}
};
this.history = {
pushState: function() {
this.replaceState.apply(this, arguments);
historyEntriesLength++;
},
replaceState: function(state, title, url) {
locationHref.href = url;
if (!options.updateAsync) committedHref.href = locationHref.href;
mockWindow.history.state = copy(state);
if (!options.updateAsync) this.flushHref();
},
flushHref: function() {
committedHref.href = locationHref.href;
}
};
// IE 10-11 deserialize history.state on each read making subsequent reads
// different object.
if (!msie) {
this.history.state = null;
} else {
ieState = null;
Object.defineProperty(this.history, 'state', {
get: function() {
return copy(ieState);
},
set: function(value) {
ieState = value;
},
configurable: true,
enumerable: true
});
}
}
function MockDocument() {
var self = this;
this[0] = window.document;
this.basePath = '/';
this.find = function(name) {
if (name === 'base') {
return {
attr: function(name) {
if (name === 'href') {
return self.basePath;
} else {
throw new Error(name);
}
}
};
} else {
throw new Error(name);
}
};
}
describe('browser', function() {
/* global Browser: false, TaskTracker: false */
var browser, fakeWindow, fakeDocument, fakeLog, logs, taskTrackerFactory;
beforeEach(function() {
sniffer = {history: true};
fakeWindow = new MockWindow();
fakeDocument = new MockDocument();
taskTrackerFactory = function(log) { return new TaskTracker(log); };
logs = {log:[], warn:[], info:[], error:[]};
fakeLog = {
log: function() { logs.log.push(slice.call(arguments)); },
warn: function() { logs.warn.push(slice.call(arguments)); },
info: function() { logs.info.push(slice.call(arguments)); },
error: function() { logs.error.push(slice.call(arguments)); }
};
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
describe('MockBrowser', function() {
describe('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);
});
});
describe('in IE', runTests({msie: true}));
describe('not in IE', runTests({msie: false}));
function runTests(options) {
return function() {
it('should return the same state object on every read', function() {
var msie = options.msie;
fakeWindow = new MockWindow({msie: msie});
fakeWindow.location.state = {prop: 'val'};
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
browser.url(fakeWindow.location.href, false, {prop: 'val'});
if (msie) {
expect(fakeWindow.history.state).not.toBe(fakeWindow.history.state);
expect(fakeWindow.history.state).toEqual(fakeWindow.history.state);
} else {
expect(fakeWindow.history.state).toBe(fakeWindow.history.state);
}
});
};
}
});
describe('notifyWhenNoOutstandingRequests', function() {
it('should invoke callbacks immediately if there are no pending tasks', function() {
var callback = jasmine.createSpy('callback');
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).toHaveBeenCalled();
});
it('should invoke callbacks immediately if there are no pending tasks (for specific task-type)',
function() {
var callbackAll = jasmine.createSpy('callbackAll');
var callbackFoo = jasmine.createSpy('callbackFoo');
browser.$$incOutstandingRequestCount();
browser.notifyWhenNoOutstandingRequests(callbackAll);
browser.notifyWhenNoOutstandingRequests(callbackFoo, 'foo');
expect(callbackAll).not.toHaveBeenCalled();
expect(callbackFoo).toHaveBeenCalled();
}
);
it('should invoke callbacks as soon as there are no pending tasks', function() {
var callback = jasmine.createSpy('callback');
browser.$$incOutstandingRequestCount();
browser.notifyWhenNoOutstandingRequests(callback);
expect(callback).not.toHaveBeenCalled();
browser.$$completeOutstandingRequest(noop);
expect(callback).toHaveBeenCalled();
});
it('should invoke callbacks as soon as there are no pending tasks (for specific task-type)',
function() {
var callbackAll = jasmine.createSpy('callbackAll');
var callbackFoo = jasmine.createSpy('callbackFoo');
browser.$$incOutstandingRequestCount();
browser.$$incOutstandingRequestCount('foo');
browser.notifyWhenNoOutstandingRequests(callbackAll);
browser.notifyWhenNoOutstandingRequests(callbackFoo, 'foo');
expect(callbackAll).not.toHaveBeenCalled();
expect(callbackFoo).not.toHaveBeenCalled();
browser.$$completeOutstandingRequest(noop, 'foo');
expect(callbackAll).not.toHaveBeenCalled();
expect(callbackFoo).toHaveBeenCalledOnce();
browser.$$completeOutstandingRequest(noop);
expect(callbackAll).toHaveBeenCalledOnce();
expect(callbackFoo).toHaveBeenCalledOnce();
}
);
});
describe('defer', function() {
it('should execute fn asynchronously via setTimeout', function() {
var callback = jasmine.createSpy('deferred');
browser.defer(callback);
expect(callback).not.toHaveBeenCalled();
fakeWindow.setTimeout.flush();
expect(callback).toHaveBeenCalledOnce();
});
it('should update outstandingRequests counter', function() {
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
browser.defer(noop);
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingTasksSpy).not.toHaveBeenCalled();
fakeWindow.setTimeout.flush();
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
it('should update outstandingRequests counter (for specific task-type)', function() {
var noPendingFooTasksSpy = jasmine.createSpy('noPendingFooTasks');
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
browser.defer(noop, 0, 'foo');
browser.defer(noop, 0, 'bar');
browser.notifyWhenNoOutstandingRequests(noPendingFooTasksSpy, 'foo');
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingFooTasksSpy).not.toHaveBeenCalled();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
fakeWindow.setTimeout.flush(1);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
fakeWindow.setTimeout.flush(1);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
it('should return unique deferId', function() {
var deferId1 = browser.defer(noop),
deferId2 = browser.defer(noop);
expect(deferId1).toBeDefined();
expect(deferId2).toBeDefined();
expect(deferId1).not.toEqual(deferId2);
});
describe('cancel', function() {
it('should allow tasks to be canceled with returned deferId', function() {
var log = [],
deferId1 = browser.defer(function() { log.push('cancel me'); }),
deferId2 = browser.defer(function() { log.push('ok'); }),
deferId3 = browser.defer(function() { log.push('cancel me, now!'); });
expect(log).toEqual([]);
expect(browser.defer.cancel(deferId1)).toBe(true);
expect(browser.defer.cancel(deferId3)).toBe(true);
fakeWindow.setTimeout.flush();
expect(log).toEqual(['ok']);
expect(browser.defer.cancel(deferId2)).toBe(false);
});
it('should update outstandingRequests counter', function() {
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
var deferId = browser.defer(noop);
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingTasksSpy).not.toHaveBeenCalled();
browser.defer.cancel(deferId);
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
it('should update outstandingRequests counter (for specific task-type)', function() {
var noPendingFooTasksSpy = jasmine.createSpy('noPendingFooTasks');
var noPendingTasksSpy = jasmine.createSpy('noPendingTasks');
var deferId1 = browser.defer(noop, 0, 'foo');
var deferId2 = browser.defer(noop, 0, 'bar');
browser.notifyWhenNoOutstandingRequests(noPendingFooTasksSpy, 'foo');
browser.notifyWhenNoOutstandingRequests(noPendingTasksSpy);
expect(noPendingFooTasksSpy).not.toHaveBeenCalled();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
browser.defer.cancel(deferId1);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).not.toHaveBeenCalled();
browser.defer.cancel(deferId2);
expect(noPendingFooTasksSpy).toHaveBeenCalledOnce();
expect(noPendingTasksSpy).toHaveBeenCalledOnce();
});
});
});
describe('url', function() {
var pushState, replaceState, locationReplace;
beforeEach(function() {
pushState = spyOn(fakeWindow.history, 'pushState');
replaceState = spyOn(fakeWindow.history, 'replaceState');
locationReplace = spyOn(fakeWindow.location, 'replace');
});
it('should return current location.href', function() {
fakeWindow.location.href = 'http://test.com';
expect(browser.url()).toEqual('http://test.com/');
fakeWindow.location.href = 'https://another.com';
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');
expect(pushState).toHaveBeenCalledOnce();
expect(pushState.calls.argsFor(0)[2]).toEqual('http://new.org/');
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
expect(fakeWindow.location.href).toEqual('http://server/');
});
it('should use history.replaceState when available', function() {
sniffer.history = true;
browser.url('http://new.org', true);
expect(replaceState).toHaveBeenCalledOnce();
expect(replaceState.calls.argsFor(0)[2]).toEqual('http://new.org/');
expect(pushState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
expect(fakeWindow.location.href).toEqual('http://server/');
});
it('should set location.href when pushState not available', function() {
sniffer.history = false;
browser.url('http://new.org');
expect(fakeWindow.location.href).toEqual('http://new.org/');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should set location.href and not use pushState when the url only changed in the hash fragment to please IE10/11', function() {
sniffer.history = true;
browser.url('http://server/#123');
expect(fakeWindow.location.href).toEqual('http://server/#123');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should retain the # character when the only change is clearing the hash fragment, to prevent page reload', function() {
sniffer.history = true;
browser.url('http://server/#123');
expect(fakeWindow.location.href).toEqual('http://server/#123');
browser.url('http://server/');
expect(fakeWindow.location.href).toEqual('http://server/#');
});
it('should use location.replace when history.replaceState not available', function() {
sniffer.history = false;
browser.url('http://new.org', true);
expect(locationReplace).toHaveBeenCalledWith('http://new.org/');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(fakeWindow.location.href).toEqual('http://server/');
});
it('should use location.replace and not use replaceState when the url only changed in the hash fragment to please IE10/11', function() {
sniffer.history = true;
browser.url('http://server/#123', true);
expect(locationReplace).toHaveBeenCalledWith('http://server/#123');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(fakeWindow.location.href).toEqual('http://server/');
});
it('should return $browser to allow chaining', function() {
expect(browser.url('http://any.com')).toBe(browser);
});
it('should return $browser to allow chaining even if the previous and current URLs and states match', function() {
expect(browser.url('http://any.com').url('http://any.com')).toBe(browser);
var state = { any: 'foo' };
expect(browser.url('http://any.com', false, state).url('http://any.com', false, state)).toBe(browser);
expect(browser.url('http://any.com', true, state).url('http://any.com', true, state)).toBe(browser);
});
it('should not set URL when the URL is already set', function() {
var current = fakeWindow.location.href;
sniffer.history = false;
fakeWindow.location.href = 'http://dontchange/';
browser.url(current);
expect(fakeWindow.location.href).toBe('http://dontchange/');
});
it('should not read out location.href if a reload was triggered but still allow to change the url', function() {
sniffer.history = false;
browser.url('http://server/someOtherUrlThatCausesReload');
expect(fakeWindow.location.href).toBe('http://server/someOtherUrlThatCausesReload');
fakeWindow.location.href = 'http://someNewUrl';
expect(browser.url()).toBe('http://server/someOtherUrlThatCausesReload');
browser.url('http://server/someOtherUrl');
expect(browser.url()).toBe('http://server/someOtherUrl');
expect(fakeWindow.location.href).toBe('http://server/someOtherUrl');
});
it('assumes that changes to location.hash occur in sync', function(done) {
// This is an asynchronous integration test that changes the
// hash in all possible ways and checks
// - whether the change to the hash can be read out in sync
// - whether the change to the hash can be read out in the hashchange event
var realWin = window,
$realWin = jqLite(realWin),
hashInHashChangeEvent = [];
var job = createAsync(done);
job.runs(function() {
$realWin.on('hashchange', hashListener);
realWin.location.hash = '1';
realWin.location.href += '2';
realWin.location.replace(realWin.location.href + '3');
realWin.location.assign(realWin.location.href + '4');
expect(realWin.location.hash).toBe('#1234');
})
.waitsFor(function() {
return hashInHashChangeEvent.length > 3;
})
.runs(function() {
$realWin.off('hashchange', hashListener);
forEach(hashInHashChangeEvent, function(hash) {
expect(hash).toBe('#1234');
});
}).done();
job.start();
function hashListener() {
hashInHashChangeEvent.push(realWin.location.hash);
}
});
});
describe('url (with ie 11 weirdnesses)', function() {
it('url() should actually set the url, even if IE 11 is weird and replaces HTML entities in the URL', function() {
// this test can not be expressed with the Jasmine spies in the previous describe block, because $browser.url()
// needs to observe the change to location.href during its invocation to enter the failing code path, but the spies
// are not callThrough
sniffer.history = true;
var originalReplace = fakeWindow.location.replace;
fakeWindow.location.replace = function(url) {
url = url.replace('¬', '¬');
// I really don't know why IE 11 (sometimes) does this, but I am not the only one to notice:
// https://connect.microsoft.com/IE/feedback/details/1040980/bug-in-ie-which-interprets-document-location-href-as-html
originalReplace.call(this, url);
};
// the initial URL contains a lengthy oauth token in the hash
var initialUrl = 'http://test.com/oauthcallback#state=xxx%3D¬-before-policy=0';
fakeWindow.location.href = initialUrl;
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
// somehow, $location gets a version of this url where the = is no longer escaped, and tells the browser:
var initialUrlFixedByLocation = initialUrl.replace('%3D', '=');
browser.url(initialUrlFixedByLocation, true, null);
expect(browser.url()).toEqual(initialUrlFixedByLocation);
// a little later (but in the same digest cycle) the view asks $location to replace the url, which tells $browser
var secondUrl = 'http://test.com/otherView';
browser.url(secondUrl, true, null);
expect(browser.url()).toEqual(secondUrl);
});
});
describe('url (when state passed)', function() {
var currentHref, pushState, replaceState, locationReplace;
beforeEach(function() {
});
describe('in IE', runTests({msie: true}));
describe('not in IE', runTests({msie: false}));
function runTests(options) {
return function() {
beforeEach(function() {
sniffer = {history: true};
fakeWindow = new MockWindow({msie: options.msie});
currentHref = fakeWindow.location.href;
pushState = spyOn(fakeWindow.history, 'pushState').and.callThrough();
replaceState = spyOn(fakeWindow.history, 'replaceState').and.callThrough();
locationReplace = spyOn(fakeWindow.location, 'replace').and.callThrough();
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
browser.onUrlChange(function() {});
});
it('should change state', function() {
browser.url(currentHref, false, {prop: 'val1'});
expect(fakeWindow.history.state).toEqual({prop: 'val1'});
browser.url(currentHref + '/something', false, {prop: 'val2'});
expect(fakeWindow.history.state).toEqual({prop: 'val2'});
});
it('should allow to set falsy states (except `undefined`)', function() {
fakeWindow.history.state = {prop: 'val1'};
fakeWindow.fire('popstate');
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'};
fakeWindow.fire('popstate');
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 deep equal but referentially different state', function() {
fakeWindow.history.state = {prop: 'val'};
fakeWindow.fire('popstate');
expect(historyEntriesLength).toBe(1);
browser.url(currentHref, false, {prop: 'val'});
expect(fakeWindow.history.state).toEqual({prop: 'val'});
expect(historyEntriesLength).toBe(2);
});
it('should not do pushState with the same URL and state from $browser.state()', function() {
browser.url(currentHref, false, {prop: 'val'});
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
browser.url(currentHref, false, browser.state());
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should not do pushState with a URL using relative protocol', function() {
browser.url('http://server/');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
browser.url('//server');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should not do pushState with a URL only adding a trailing slash after domain', function() {
// A domain without a trailing /
browser.url('http://server');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
// A domain from something such as window.location.href with a trailing slash
browser.url('http://server/');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should not do pushState with a URL only removing a trailing slash after domain', function() {
// A domain from something such as window.location.href with a trailing slash
browser.url('http://server/');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
// A domain without a trailing /
browser.url('http://server');
expect(pushState).not.toHaveBeenCalled();
expect(replaceState).not.toHaveBeenCalled();
expect(locationReplace).not.toHaveBeenCalled();
});
it('should do pushState with a URL only adding a trailing slash after the path', function() {
browser.url('http://server/foo');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
browser.url('http://server/foo/');
expect(pushState).toHaveBeenCalledOnce();
expect(fakeWindow.location.href).toEqual('http://server/foo/');
});
it('should do pushState with a URL only removing a trailing slash after the path', function() {
browser.url('http://server/foo/');
pushState.calls.reset();
replaceState.calls.reset();
locationReplace.calls.reset();
browser.url('http://server/foo');
expect(pushState).toHaveBeenCalledOnce();
expect(fakeWindow.location.href).toEqual('http://server/foo');
});
};
}
});
describe('state', function() {
var currentHref;
beforeEach(function() {
sniffer = {history: true};
currentHref = fakeWindow.location.href;
});
it('should not access `history.state` when `$sniffer.history` is false', function() {
// In the context of a Chrome Packaged App, although `history.state` is present, accessing it
// is not allowed and logs an error in the console. We should not try to access
// `history.state` in contexts where `$sniffer.history` is false.
var historyStateAccessed = false;
var mockSniffer = {history: false};
var mockWindow = new MockWindow();
var _state = mockWindow.history.state;
Object.defineProperty(mockWindow.history, 'state', {
get: function() {
historyStateAccessed = true;
return _state;
}
});
var browser = new Browser(mockWindow, fakeDocument, fakeLog, mockSniffer, taskTrackerFactory);
expect(historyStateAccessed).toBe(false);
});
describe('in IE', runTests({msie: true}));
describe('not in IE', runTests({msie: false}));
function runTests(options) {
return function() {
beforeEach(function() {
fakeWindow = new MockWindow({msie: options.msie});
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
it('should return history.state', function() {
browser.url(currentHref, false, {prop: 'val'});
expect(browser.state()).toEqual({prop: 'val'});
browser.url(currentHref, false, 2);
expect(browser.state()).toEqual(2);
browser.url(currentHref, false, null);
expect(browser.state()).toEqual(null);
});
it('should return null if history.state is undefined', function() {
browser.url(currentHref, false, undefined);
expect(browser.state()).toBe(null);
});
it('should return the same state object in subsequent invocations in IE', function() {
browser.url(currentHref, false, {prop: 'val'});
expect(browser.state()).toBe(browser.state());
});
};
}
});
describe('urlChange', function() {
var callback;
beforeEach(function() {
callback = jasmine.createSpy('onUrlChange');
});
afterEach(function() {
if (!jQuery) jqLiteDealoc(fakeWindow);
});
it('should return registered callback', function() {
expect(browser.onUrlChange(callback)).toBe(callback);
});
it('should forward popstate event with new url when history supported', function() {
sniffer.history = true;
browser.onUrlChange(callback);
fakeWindow.location.href = 'http://server/new';
fakeWindow.fire('popstate');
expect(callback).toHaveBeenCalledWith('http://server/new', null);
fakeWindow.fire('hashchange');
fakeWindow.setTimeout.flush();
expect(callback).toHaveBeenCalledOnce();
});
it('should forward only popstate event when history supported', function() {
sniffer.history = true;
browser.onUrlChange(callback);
fakeWindow.location.href = 'http://server/new';
fakeWindow.fire('popstate');
expect(callback).toHaveBeenCalledWith('http://server/new', null);
fakeWindow.fire('hashchange');
fakeWindow.setTimeout.flush();
expect(callback).toHaveBeenCalledOnce();
});
it('should forward hashchange event with new url when history not supported', function() {
sniffer.history = false;
browser.onUrlChange(callback);
fakeWindow.location.href = 'http://server/new';
fakeWindow.fire('hashchange');
expect(callback).toHaveBeenCalledWith('http://server/new', null);
fakeWindow.fire('popstate');
fakeWindow.setTimeout.flush();
expect(callback).toHaveBeenCalledOnce();
});
it('should not fire urlChange if changed by browser.url method', function() {
sniffer.history = false;
browser.onUrlChange(callback);
browser.url('http://new.com/');
fakeWindow.fire('hashchange');
expect(callback).not.toHaveBeenCalled();
});
describe('state handling', function() {
var currentHref;
beforeEach(function() {
sniffer = {history: true};
currentHref = fakeWindow.location.href;
});
describe('in IE', runTests({msie: true}));
describe('not in IE', runTests({msie: false}));
function runTests(options) {
return function() {
beforeEach(function() {
fakeWindow = new MockWindow({msie: options.msie});
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
});
it('should fire onUrlChange listeners only once if both popstate and hashchange triggered', function() {
fakeWindow.history.state = {prop: 'val'};
browser.onUrlChange(callback);
fakeWindow.fire('hashchange');
fakeWindow.fire('popstate');
expect(callback).toHaveBeenCalledOnce();
});
};
}
});
it('should stop calling callbacks when application has been torn down', function() {
sniffer.history = true;
browser.onUrlChange(callback);
fakeWindow.location.href = 'http://server/new';
browser.$$applicationDestroyed();
fakeWindow.fire('popstate');
expect(callback).not.toHaveBeenCalled();
fakeWindow.fire('hashchange');
fakeWindow.setTimeout.flush();
expect(callback).not.toHaveBeenCalled();
});
});
describe('baseHref', function() {
var jqDocHead;
beforeEach(function() {
jqDocHead = jqLite(window.document).find('head');
});
it('should return value from ', function() {
fakeDocument.basePath = '/base/path/';
expect(browser.baseHref()).toEqual('/base/path/');
});
it('should return \'\' (empty string) if no ', function() {
fakeDocument.basePath = undefined;
expect(browser.baseHref()).toEqual('');
});
it('should remove domain from ', function() {
fakeDocument.basePath = 'http://host.com/base/path/';
expect(browser.baseHref()).toEqual('/base/path/');
fakeDocument.basePath = 'http://host.com/base/path/index.html';
expect(browser.baseHref()).toEqual('/base/path/index.html');
});
it('should remove domain from beginning with \'//\'', function() {
fakeDocument.basePath = '//google.com/base/path/';
expect(browser.baseHref()).toEqual('/base/path/');
});
});
describe('integration tests with $location', function() {
function setup(options) {
fakeWindow = new MockWindow(options);
browser = new Browser(fakeWindow, fakeDocument, fakeLog, sniffer, taskTrackerFactory);
module(function($provide, $locationProvider) {
spyOn(fakeWindow.history, 'pushState').and.callFake(function(stateObj, title, newUrl) {
fakeWindow.location.href = newUrl;
});
spyOn(fakeWindow.location, 'replace').and.callFake(function(newUrl) {
fakeWindow.location.href = newUrl;
});
$provide.value('$browser', browser);
sniffer.history = options.history;
$provide.value('$sniffer', sniffer);
$locationProvider.html5Mode(options.html5Mode);
});
}
describe('update $location when it was changed outside of AngularJS in sync ' +
'before $digest was called', function() {
it('should work with no history support, no html5Mode', function() {
setup({
history: false,
html5Mode: false
});
inject(function($rootScope, $location) {
$rootScope.$apply(function() {
$location.path('/initialPath');
});
expect(fakeWindow.location.href).toBe('http://server/#!/initialPath');
fakeWindow.location.href = 'http://server/#!/someTestHash';
$rootScope.$digest();
expect($location.path()).toBe('/someTestHash');
});
});
it('should work with history support, no html5Mode', function() {
setup({
history: true,
html5Mode: false
});
inject(function($rootScope, $location) {
$rootScope.$apply(function() {
$location.path('/initialPath');
});
expect(fakeWindow.location.href).toBe('http://server/#!/initialPath');
fakeWindow.location.href = 'http://server/#!/someTestHash';
$rootScope.$digest();
expect($location.path()).toBe('/someTestHash');
});
});
it('should work with no history support, with html5Mode', function() {
setup({
history: false,
html5Mode: true
});
inject(function($rootScope, $location) {
$rootScope.$apply(function() {
$location.path('/initialPath');
});
expect(fakeWindow.location.href).toBe('http://server/#!/initialPath');
fakeWindow.location.href = 'http://server/#!/someTestHash';
$rootScope.$digest();
expect($location.path()).toBe('/someTestHash');
});
});
it('should work with history support, with html5Mode', function() {
setup({
history: true,
html5Mode: true
});
inject(function($rootScope, $location) {
$rootScope.$apply(function() {
$location.path('/initialPath');
});
expect(fakeWindow.location.href).toBe('http://server/initialPath');
fakeWindow.location.href = 'http://server/someTestHash';
$rootScope.$digest();
expect($location.path()).toBe('/someTestHash');
});
});
});
it('should not reload the page on every $digest when the page will be reloaded due to url rewrite on load', function() {
setup({
history: false,
html5Mode: true
});
fakeWindow.location.href = 'http://server/some/deep/path';
var changeUrlCount = 0;
var _url = browser.url;
browser.url = function(newUrl, replace, state) {
if (newUrl) {
changeUrlCount++;
}
return _url.call(this, newUrl, replace);
};
spyOn(browser, 'url').and.callThrough();
inject(function($rootScope, $location) {
$rootScope.$digest();
$rootScope.$digest();
$rootScope.$digest();
$rootScope.$digest();
// from $location for rewriting the initial url into a hash url
expect(browser.url).toHaveBeenCalledWith('http://server/#!/some/deep/path', true);
expect(changeUrlCount).toBe(1);
});
});
// issue #12241
it('should not infinite digest if the browser does not synchronously update the location properties', function() {
setup({
history: true,
html5Mode: true,
updateAsync: true // Simulate a browser that doesn't update the href synchronously
});
inject(function($location, $rootScope) {
// Change the hash within AngularJS and check that we don't infinitely digest
$location.hash('newHash');
expect(function() { $rootScope.$digest(); }).not.toThrow();
expect($location.absUrl()).toEqual('http://server/#newHash');
// Now change the hash from outside AngularJS and check that $location updates correctly
fakeWindow.location.hash = '#otherHash';
// simulate next tick - since this browser doesn't update synchronously
fakeWindow.location.flushHref();
fakeWindow.fire('hashchange');
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();
var 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() {
beforeEach(module(function($provide, $locationProvider) {
$provide.value('$browser', browser);
}));
it('should not interfere with legacy browser url replace behavior', function() {
inject(function($rootScope) {
var current = fakeWindow.location.href;
var newUrl = 'http://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(historyEntriesLength).toBe(1);
});
});
});
});