diff --git a/src/ng/browser.js b/src/ng/browser.js index 55090832a740..c98a12418b9c 100644 --- a/src/ng/browser.js +++ b/src/ng/browser.js @@ -87,7 +87,14 @@ function Browser(window, document, $log, $sniffer) { var cachedState, lastHistoryState, lastBrowserUrl = location.href, baseElement = document.find('base'), - pendingLocation = null; + pendingLocation = null, + getCurrentState = !$sniffer.history ? noop : function getCurrentState() { + try { + return history.state; + } catch (e) { + // MSIE can reportedly throw when there is no state (UNCONFIRMED). + } + }; cacheState(); lastHistoryState = cachedState; @@ -195,14 +202,6 @@ function Browser(window, document, $log, $sniffer) { fireUrlChange(); } - function getCurrentState() { - try { - return history.state; - } catch (e) { - // MSIE can reportedly throw when there is no state (UNCONFIRMED). - } - } - // This variable should be used *only* inside the cacheState function. var lastCachedState = null; function cacheState() { diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js index 7c9f5ee3b26a..0c5ac51d26ff 100644 --- a/src/ng/sniffer.js +++ b/src/ng/sniffer.js @@ -17,6 +17,10 @@ function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, + // Chrome Packaged Apps are not allowed to access `history.pushState`. They can be detected by + // the presence of `chrome.app.runtime` (see https://developer.chrome.com/apps/api_index) + isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime, + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), @@ -61,7 +65,7 @@ function $SnifferProvider() { // so let's not use the history API also // We are purposefully using `!(android < 4)` to cover the case when `android` is undefined // jshint -W018 - history: !!($window.history && $window.history.pushState && !(android < 4) && !boxee), + history: !!(hasHistoryPushState && !(android < 4) && !boxee), // jshint +W018 hasEvent: function(event) { // IE9 implements 'input' event it's so fubared that we rather pretend that it doesn't have diff --git a/test/ng/browserSpecs.js b/test/ng/browserSpecs.js index 295f285bf46a..51f928526ba5 100755 --- a/test/ng/browserSpecs.js +++ b/test/ng/browserSpecs.js @@ -537,9 +537,32 @@ describe('browser', function() { 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 = {histroy: 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); + + expect(historyStateAccessed).toBe(false); + }); + describe('in IE', runTests({msie: true})); describe('not in IE', runTests({msie: false})); + function runTests(options) { return function() { beforeEach(function() { diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js index b1fafceff36d..31f693e7ab33 100644 --- a/test/ng/snifferSpec.js +++ b/test/ng/snifferSpec.js @@ -1,10 +1,9 @@ 'use strict'; describe('$sniffer', function() { - function sniffer($window, $document) { /* global $SnifferProvider: false */ - $window.navigator = {}; + $window.navigator = $window.navigator || {}; $document = jqLite($document || {}); if (!$document[0].body) { $document[0].body = window.document.body; @@ -12,14 +11,84 @@ describe('$sniffer', function() { return new $SnifferProvider().$get[2]($window, $document); } + describe('history', function() { it('should be true if history.pushState defined', function() { - expect(sniffer({history: {pushState: noop, replaceState: noop}}).history).toBe(true); + var mockWindow = { + history: { + pushState: noop, + replaceState: noop + } + }; + + expect(sniffer(mockWindow).history).toBe(true); }); + it('should be false if history or pushState not defined', function() { - expect(sniffer({history: {}}).history).toBe(false); expect(sniffer({}).history).toBe(false); + expect(sniffer({history: {}}).history).toBe(false); + }); + + + it('should be false on Boxee box with an older version of Webkit', function() { + var mockWindow = { + history: { + pushState: noop + }, + navigator: { + userAgent: 'boxee (alpha/Darwin 8.7.1 i386 - 0.9.11.5591)' + } + }; + + expect(sniffer(mockWindow).history).toBe(false); + }); + + + it('should be false on Chrome Packaged Apps', function() { + // Chrome Packaged Apps are not allowed to access `window.history.pushState`. + // In Chrome, `window.app` might be available in "normal" webpages, but `window.app.runtime` + // only exists in the context of a packaged app. + + expect(sniffer(createMockWindow()).history).toBe(true); + expect(sniffer(createMockWindow(true)).history).toBe(true); + expect(sniffer(createMockWindow(true, true)).history).toBe(false); + + function createMockWindow(isChrome, isPackagedApp) { + var mockWindow = { + history: { + pushState: noop + } + }; + + if (isChrome) { + var chromeAppObj = isPackagedApp ? {runtime: {}} : {}; + mockWindow.chrome = {app: chromeAppObj}; + } + + return mockWindow; + } + }); + + + it('should not try to access `history.pushState` in Chrome Packaged Apps', function() { + var pushStateAccessCount = 0; + + var mockHistory = Object.create(Object.prototype, { + pushState: {get: function() { pushStateAccessCount++; return noop; }} + }); + var mockWindow = { + chrome: { + app: { + runtime: {} + } + }, + history: mockHistory + }; + + sniffer(mockWindow); + + expect(pushStateAccessCount).toBe(0); }); }); @@ -28,11 +97,10 @@ describe('$sniffer', function() { var mockDocument, mockDivElement, $sniffer; beforeEach(function() { - mockDocument = {createElement: jasmine.createSpy('createElement')}; - mockDocument.createElement.andCallFake(function(elm) { - if (elm === 'div') return mockDivElement; - }); + var mockCreateElementFn = function(elm) { if (elm === 'div') return mockDivElement; }; + var createElementSpy = jasmine.createSpy('createElement').andCallFake(mockCreateElementFn); + mockDocument = {createElement: createElementSpy}; $sniffer = sniffer({}, mockDocument); }); @@ -83,7 +151,6 @@ describe('$sniffer', function() { describe('vendorPrefix', function() { - it('should return the correct vendor prefix based on the browser', function() { inject(function($sniffer, $window) { var expectedPrefix; @@ -101,237 +168,171 @@ describe('$sniffer', function() { }); }); + it('should still work for an older version of Webkit', function() { - module(function($provide) { - var doc = { - body: { - style: { - WebkitOpacity: '0' - } + var mockDocument = { + body: { + style: { + WebkitOpacity: '0' } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.vendorPrefix).toBe('webkit'); - }); - }); + } + }; + expect(sniffer({}, mockDocument).vendorPrefix).toBe('webkit'); + }); }); + describe('animations', function() { - it('should be either true or false', function() { - inject(function($sniffer) { - expect($sniffer.animations).not.toBe(undefined); - }); - }); + it('should be either true or false', inject(function($sniffer) { + expect($sniffer.animations).toBeDefined(); + })); + it('should be false when there is no animation style', function() { - module(function($provide) { - var doc = { - body: { - style: {} - } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.animations).toBe(false); - }); + var mockDocument = { + body: { + style: {} + } + }; + + expect(sniffer({}, mockDocument).animations).toBe(false); }); + it('should be true with vendor-specific animations', function() { - module(function($provide) { - var animationStyle = 'some_animation 2s linear'; - var doc = { - body: { - style: { - WebkitAnimation: animationStyle, - MozAnimation: animationStyle - } + var animationStyle = 'some_animation 2s linear'; + var mockDocument = { + body: { + style: { + WebkitAnimation: animationStyle, + MozAnimation: animationStyle } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.animations).toBe(true); - }); + } + }; + + expect(sniffer({}, mockDocument).animations).toBe(true); }); + it('should be true with w3c-style animations', function() { - module(function($provide) { - var doc = { - body: { - style: { - animation: 'some_animation 2s linear' - } + var mockDocument = { + body: { + style: { + animation: 'some_animation 2s linear' } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.animations).toBe(true); - }); + } + }; + + expect(sniffer({}, mockDocument).animations).toBe(true); }); + it('should be true on android with older body style properties', function() { - module(function($provide) { - var doc = { - body: { - style: { - webkitAnimation: '' - } - } - }; - var win = { - navigator: { - userAgent: 'android 2' + var mockWindow = { + navigator: { + userAgent: 'android 2' + } + }; + var mockDocument = { + body: { + style: { + webkitAnimation: '' } - }; - $provide.value('$document', jqLite(doc)); - $provide.value('$window', win); - }); - inject(function($sniffer) { - expect($sniffer.animations).toBe(true); - }); + } + }; + + expect(sniffer(mockWindow, mockDocument).animations).toBe(true); }); + it('should be true when an older version of Webkit is used', function() { - module(function($provide) { - var doc = { - body: { - style: { - WebkitOpacity: '0' - } + var mockDocument = { + body: { + style: { + WebkitOpacity: '0' } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.animations).toBe(false); - }); - }); + } + }; + expect(sniffer({}, mockDocument).animations).toBe(false); + }); }); + describe('transitions', function() { + it('should be either true or false', inject(function($sniffer) { + expect($sniffer.transitions).toBeOneOf(true, false); + })); - it('should be either true or false', function() { - inject(function($sniffer) { - expect($sniffer.transitions).not.toBe(undefined); - }); - }); it('should be false when there is no transition style', function() { - module(function($provide) { - var doc = { - body: { - style: {} - } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.transitions).toBe(false); - }); + var mockDocument = { + body: { + style: {} + } + }; + + expect(sniffer({}, mockDocument).transitions).toBe(false); }); + it('should be true with vendor-specific transitions', function() { - module(function($provide) { - var transitionStyle = '1s linear all'; - var doc = { - body: { - style: { - WebkitTransition: transitionStyle, - MozTransition: transitionStyle - } + var transitionStyle = '1s linear all'; + var mockDocument = { + body: { + style: { + WebkitTransition: transitionStyle, + MozTransition: transitionStyle } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.transitions).toBe(true); - }); + } + }; + + expect(sniffer({}, mockDocument).transitions).toBe(true); }); + it('should be true with w3c-style transitions', function() { - module(function($provide) { - var doc = { - body: { - style: { - transition: '1s linear all' - } + var mockDocument = { + body: { + style: { + transition: '1s linear all' } - }; - $provide.value('$document', jqLite(doc)); - }); - inject(function($sniffer) { - expect($sniffer.transitions).toBe(true); - }); - }); + } + }; - it('should be true on android with older body style properties', function() { - module(function($provide) { - var doc = { - body: { - style: { - webkitTransition: '' - } - } - }; - var win = { - navigator: { - userAgent: 'android 2' - } - }; - $provide.value('$document', jqLite(doc)); - $provide.value('$window', win); - }); - inject(function($sniffer) { - expect($sniffer.transitions).toBe(true); - }); + expect(sniffer({}, mockDocument).transitions).toBe(true); }); - }); - - describe('history', function() { - it('should be true on Boxee box with an older version of Webkit', function() { - module(function($provide) { - var doc = { - body: { - style: {} - } - }; - var win = { - history: { - pushState: noop - }, - navigator: { - userAgent: 'boxee (alpha/Darwin 8.7.1 i386 - 0.9.11.5591)' + it('should be true on android with older body style properties', function() { + var mockWindow = { + navigator: { + userAgent: 'android 2' + } + }; + var mockDocument = { + body: { + style: { + webkitTransition: '' } - }; - $provide.value('$document', jqLite(doc)); - $provide.value('$window', win); - }); - inject(function($sniffer) { - expect($sniffer.history).toBe(false); - }); + } + }; + + expect(sniffer(mockWindow, mockDocument).transitions).toBe(true); }); }); - it('should provide the android version', function() { - module(function($provide) { - var win = { + + describe('android', function() { + it('should provide the android version', function() { + var mockWindow = { navigator: { userAgent: 'android 2' } }; - $provide.value('$document', jqLite({})); - $provide.value('$window', win); - }); - inject(function($sniffer) { - expect($sniffer.android).toBe(2); + + expect(sniffer(mockWindow).android).toBe(2); }); }); });