From 566b31ce7e470cbafcf6dfee38280bce45f01501 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 4 Feb 2016 12:53:44 +0200 Subject: [PATCH 1/4] test($sniffer): make the structure and layout of tests more consistent --- test/ng/snifferSpec.js | 332 ++++++++++++++++++----------------------- 1 file changed, 143 insertions(+), 189 deletions(-) diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js index b1fafceff36d..f66c81bbd94d 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,37 @@ 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); }); }); @@ -28,11 +50,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 +104,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 +121,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); }); }); }); From 87ace903c2f5ac25ecf740f92fe15afba76b4d69 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 4 Feb 2016 12:54:29 +0200 Subject: [PATCH 2/4] fix($sniffer): fix history sniffing in Chrome Packaged Apps Although `window.history` is present in the context of Chrome Packaged Apps, it is not allowed to access `window.history.pushState` or `window.history.state`, resulting in errors when trying to "sniff" history support. This commit fixes it by detecting a Chrome Packaged App (through the presence of `window.chrome.app.runtime`). Note that `window.chrome.app` is present in the context of "normal" webpages as well, but it doesn't have the `runtime` property, which is only available to packaged apps (e.g. see https://developer.chrome.com/apps/api_index). Fixes #11932 --- src/ng/sniffer.js | 6 +++++- test/ng/snifferSpec.js | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/src/ng/sniffer.js b/src/ng/sniffer.js index 7c9f5ee3b26a..d940c8c500eb 100644 --- a/src/ng/sniffer.js +++ b/src/ng/sniffer.js @@ -17,6 +17,8 @@ function $SnifferProvider() { this.$get = ['$window', '$document', function($window, $document) { var eventSupport = {}, + isChromePackagedApp = $window.chrome && $window.chrome.app && $window.chrome.app.runtime, + hasHistoryPushState = $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), @@ -57,11 +59,13 @@ function $SnifferProvider() { // http://code.google.com/p/android/issues/detail?id=17471 // https://github.com/angular/angular.js/issues/904 + // 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) // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // 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: !!(!isChromePackagedApp && 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/snifferSpec.js b/test/ng/snifferSpec.js index f66c81bbd94d..ad2b940ee54d 100644 --- a/test/ng/snifferSpec.js +++ b/test/ng/snifferSpec.js @@ -43,6 +43,32 @@ describe('$sniffer', function() { 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; + } + }); }); From 6cab4adb9c2fd542f1393f5be798c626f4d2f000 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 4 Feb 2016 15:34:26 +0200 Subject: [PATCH 3/4] fixup 1 --- src/ng/browser.js | 17 ++++++++--------- src/ng/sniffer.js | 8 ++++---- test/ng/browserSpecs.js | 23 +++++++++++++++++++++++ test/ng/snifferSpec.js | 21 +++++++++++++++++++++ 4 files changed, 56 insertions(+), 13 deletions(-) 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 d940c8c500eb..0c5ac51d26ff 100644 --- a/src/ng/sniffer.js +++ b/src/ng/sniffer.js @@ -17,8 +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 = $window.history && $window.history.pushState, + hasHistoryPushState = !isChromePackagedApp && $window.history && $window.history.pushState, android = toInt((/android (\d+)/.exec(lowercase(($window.navigator || {}).userAgent)) || [])[1]), boxee = /Boxee/i.test(($window.navigator || {}).userAgent), @@ -59,13 +61,11 @@ function $SnifferProvider() { // http://code.google.com/p/android/issues/detail?id=17471 // https://github.com/angular/angular.js/issues/904 - // 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) // older webkit browser (533.9) on Boxee box has exactly the same problem as Android has // 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: !!(!isChromePackagedApp && hasHistoryPushState && !(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 ad2b940ee54d..5d82097185a6 100644 --- a/test/ng/snifferSpec.js +++ b/test/ng/snifferSpec.js @@ -69,6 +69,27 @@ describe('$sniffer', function() { 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); + }); }); From 047587db2bf6dde21711778b974840f623a4c936 Mon Sep 17 00:00:00 2001 From: Georgios Kalpakas Date: Thu, 4 Feb 2016 21:01:55 +0200 Subject: [PATCH 4/4] fixup 2 --- test/ng/snifferSpec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ng/snifferSpec.js b/test/ng/snifferSpec.js index 5d82097185a6..31f693e7ab33 100644 --- a/test/ng/snifferSpec.js +++ b/test/ng/snifferSpec.js @@ -75,7 +75,7 @@ describe('$sniffer', function() { var pushStateAccessCount = 0; var mockHistory = Object.create(Object.prototype, { - pushState: {get: function() { pushStateAccessCount++; return noop; }}, + pushState: {get: function() { pushStateAccessCount++; return noop; }} }); var mockWindow = { chrome: {