/* global AppWindow, AppChrome, MocksHelper, MockL10n, PopupWindow, MockModalDialog, MockService, MockPromise, MockSettingsListener, BookmarksDatabase, Service, IconsHelper, process */ /* exported MockBookmarksDatabase */ 'use strict'; require('/shared/js/component_utils.js'); require('/shared/js/event_safety.js'); require('/shared/elements/gaia_progress/script.js'); require('/shared/elements/gaia_pin_card/script.js'); require('/shared/test/unit/mocks/mock_l10n.js'); requireApp('system/test/unit/mock_lazy_loader.js'); require('/shared/test/unit/mocks/mock_service.js'); require('/shared/test/unit/mocks/mock_promise.js'); require('/shared/test/unit/mocks/mock_icons_helper.js'); require('/shared/test/unit/mocks/mock_url_helper.js'); require('/shared/test/unit/mocks/mock_bookmarks_database.js'); requireApp('system/test/unit/mock_app_window.js'); requireApp('system/test/unit/mock_system_banner.js'); requireApp('system/test/unit/mock_popup_window.js'); requireApp('system/test/unit/mock_modal_dialog.js'); requireApp('system/shared/test/unit/mocks/mock_settings_listener.js'); function mockEvent(data) { data.stopPropagation = sinon.spy(); data.stopImmediatePropagation = sinon.spy(); data.preventDefault = sinon.spy(); data.currentTarget = data.target; return data; } const PINNING_PREF = 'dev.gaia.pinning_the_web'; var mocksForAppChrome = new MocksHelper([ 'AppWindow', 'ModalDialog', 'PopupWindow', 'BookmarksDatabase', 'Service', 'LazyLoader', 'SettingsListener', 'IconsHelper', 'SystemBanner' ]).init(); suite('system/AppChrome', function() { var stubById, realL10n, app, chrome; mocksForAppChrome.attachTestHelpers(); setup(function(done) { realL10n = navigator.mozL10n; navigator.mozL10n = MockL10n; this.sinon.useFakeTimers(); stubById = this.sinon.stub(document, 'getElementById'); stubById.returns(document.createElement('div')); requireApp('system/js/base_ui.js'); requireApp('system/js/app_chrome.js', function() { this.sinon.stub(AppChrome.prototype, 'setSiteIcon'); this.sinon.stub(AppChrome.prototype, 'setPinPreviewIcon'); app = new AppWindow(cloneConfig(fakeWebSite)); app.contextmenu = { isShown: function() {return false;} }; chrome = new AppChrome(app); done(); }.bind(this)); }); teardown(function() { navigator.mozL10n = realL10n; stubById.restore(); }); function cloneConfig(config) { return JSON.parse(JSON.stringify(config)); } var fakeWebSite = { url: 'http://google.com/index.html', origin: 'app://google.com', chrome: { scrollable: true } }; var fakeWebApp = { url: 'http://example.com/index.html', webManifestURL: 'http://example.com/manifest.webmanifest', webManifest: { 'name': 'Example Web App', 'short_name': 'Example' }, chrome: { scrollable: true } }; var fakeAppWithName = { url: 'app://communications.gaiamobile.org/dialer/index.html', name: 'Phone', manifest: {name: 'Dialer'}, manifestURL: 'app://communications.gaiamobile.org/manifest.webapp', origin: 'app://communications.gaiamobile.org', chrome: { scrollable: false } }; var fakeSearchApp = { url: 'app://search.gaiamobile.org/newtab.html', name: 'Browser', manifest: { name: 'Browser', role: 'search' }, manifestURL: 'app://search.gaiamobile.org/manifest.webapp', origin: 'app://search.gaiamobile.org', chrome: { navigation: true } }; var fakePrivateLandingPage = { url: 'app://search.gaiamobile.org/', origin: 'app://www.fake', isPrivate: true }; var fakeAppConfigBar = { url: 'app://www.fake/index.html', chrome: { bar: true } }; var fakeAppMaximized = { url: 'app://search.gaiamobile.org/newtab.html', chrome: { maximized: true } }; var fadeTransitionEndEvent = new CustomEvent('transitionend'); fadeTransitionEndEvent.propertyName = 'background-color'; test('it sets currentOrigin to the app origin', function() { assert.isTrue(chrome._currentOrigin === app.origin); }); suite('Old Navigation - Application events', function() { test('app is loading', function() { var stubShowProgress = this.sinon.stub(chrome, 'show'); var spyProgressStart = this.sinon.spy(chrome.progress, 'start'); assert.isFalse(chrome.progress.hasAttribute('animated')); chrome.handleEvent(mockEvent({ type: '_loading' })); assert.equal(chrome.progress.getAttribute('data-l10n-id'), 'gaia-progress-loading'); assert.isTrue(stubShowProgress.calledWith(chrome.progress)); assert.isTrue(spyProgressStart.calledOnce); }); test('app is loaded', function() { var stubHideProgress = this.sinon.stub(chrome, 'hide'); var spyProgressStop = this.sinon.spy(chrome.progress, 'stop'); chrome.handleEvent(mockEvent({ type: '_loaded' })); assert.equal(chrome.progress.getAttribute('data-l10n-id'), 'gaia-progress-loaded'); assert.isTrue(stubHideProgress.calledWith(chrome.progress)); assert.isTrue(spyProgressStop.calledOnce); }); test('app location is changed', function() { this.sinon.stub(chrome, 'hidePinDialogCard'); var stubHandleLocationChange = this.sinon.stub(chrome, 'handleLocationChange'); chrome.handleEvent(mockEvent({ type: '_locationchange' })); assert.isTrue(stubHandleLocationChange.called); assert.isTrue(chrome.hidePinDialogCard.called); assert.isTrue(chrome.setSiteIcon.calledWith()); }); test('app location is changed - private browser landing page', function() { var app = new AppWindow(fakePrivateLandingPage); this.sinon.stub(app, 'isBrowser').returns(true); this.sinon.stub(app, 'isPrivateBrowser').returns(true); var chrome = new AppChrome(app); this.sinon.stub(chrome, 'hidePinDialogCard'); chrome.handleEvent(mockEvent({ type: '_locationchange' })); assert.equal(chrome.title.dataset.l10nId, 'search-or-enter-address'); assert.isTrue(chrome.hidePinDialogCard.called); }); test('add bookmark', function() { var stubSelectOne = this.sinon.stub(MockModalDialog, 'selectOne'); chrome.onAddBookmark(); assert.isTrue(stubSelectOne.called); }); test('app ssl state is changed', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); this.sinon.stub(app, 'getSSLState', function() { return 'broken'; }); var chrome = new AppChrome(app); var stubHandleSecurityChanged = this.sinon.spy(chrome, 'handleSecurityChanged'); chrome.handleEvent(mockEvent({ type: '_securitychange' })); assert.isTrue(stubHandleSecurityChanged.called); assert.equal(chrome.sslIndicator.dataset.ssl, 'broken'); }); }); suite('Views', function() { test('Regular view for bar only', function() { var app = new AppWindow(fakeAppConfigBar); var spyView = this.sinon.spy(AppChrome.prototype, 'view'); new AppChrome(app); // jshint ignore:line assert.isTrue(spyView.called); }); test('Combined view for navigation + bar', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); var spyView = this.sinon.spy(AppChrome.prototype, 'combinedView'); new AppChrome(app); // jshint ignore:line assert.isTrue(spyView.called); }); }); suite('Button events', function() { test('back', function() { var stubBack = this.sinon.stub(app, 'back'); assert.equal(chrome.backButton.getAttribute('data-l10n-id'), 'back-button'); chrome.handleEvent( mockEvent({ type: 'click', target: chrome.backButton })); assert.isTrue(stubBack.called); }); test('forward', function() { var stubForward = this.sinon.stub(app, 'forward'); assert.equal(chrome.forwardButton.getAttribute('data-l10n-id'), 'forward-button'); chrome.handleEvent( mockEvent({ type: 'click', target: chrome.forwardButton })); assert.isTrue(stubForward.called); }); test('reload', function() { var stubReload = this.sinon.stub(app, 'reload'); assert.equal(chrome.reloadButton.getAttribute('data-l10n-id'), 'reload-button'); var evt = mockEvent({ type: 'click', target: chrome.reloadButton }); chrome.handleEvent(evt); assert.isTrue(stubReload.called); assert.isTrue(evt.stopPropagation.called); }); test('stop', function() { var stubStop = this.sinon.stub(app, 'stop'); assert.equal(chrome.stopButton.getAttribute('data-l10n-id'), 'stop-button'); chrome.handleEvent( mockEvent({ type: 'click', target: chrome.stopButton })); assert.isTrue(stubStop.called); }); test('windows', function(done) { var app = new AppWindow(fakeSearchApp); var chrome = new AppChrome(app); assert.equal(chrome.windowsButton.getAttribute('data-l10n-id'), 'windows-button'); window.addEventListener('taskmanagershow', function() { done(); }); chrome.handleEvent( mockEvent({ type: 'click', target: chrome.windowsButton })); }); test('location changed', function() { this.sinon.stub(app, 'isBrowser').returns(true); var stub1 = this.sinon.stub(app, 'canGoForward'); var stub2 = this.sinon.stub(app, 'canGoBack'); chrome.handleEvent(mockEvent({ type: '_locationchange' })); stub1.getCall(0).args[0](true); assert.equal(chrome.forwardButton.disabled, false); stub1.getCall(0).args[0](false); assert.equal(chrome.forwardButton.disabled, true); stub2.getCall(0).args[0](true); assert.equal(chrome.backButton.disabled, false); stub2.getCall(0).args[0](false); assert.equal(chrome.backButton.disabled, true); }); test('location#anchor changed', function() { this.sinon.stub(app, 'isBrowser').returns(true); chrome._currentURL = fakeWebSite.url; chrome.containerElement.classList.add('scrollable'); chrome.handleEvent({ type: '_locationchange', detail: fakeWebSite.url + '#anchor' }); assert.isTrue(chrome.containerElement.classList.contains('scrollable')); }); test('location changed - without navigation', function() { var app = new AppWindow(fakeAppWithName); var chrome = new AppChrome(app); this.sinon.stub(app, 'isBrowser').returns(false); var stub1 = this.sinon.stub(app, 'canGoForward'); var stub2 = this.sinon.stub(app, 'canGoBack'); var evt = new CustomEvent('_locationchange'); app.element.dispatchEvent(evt); stub1.getCall(0).args[0](true); assert.equal(chrome.forwardButton.disabled, true); stub1.getCall(0).args[0](false); assert.equal(chrome.forwardButton.disabled, true); stub2.getCall(0).args[0](true); assert.equal(chrome.backButton.disabled, true); stub2.getCall(0).args[0](false); assert.equal(chrome.backButton.disabled, true); }); }); suite('Navigation events', function() { setup(function() { chrome.setSiteIcon.reset(); chrome.setPinPreviewIcon.reset(); }); test('loadstart', function() { chrome.handleEvent(mockEvent({ type: 'mozbrowserloadstart' })); assert.isTrue(chrome.containerElement.classList.contains('loading')); assert.isFalse(chrome.setSiteIcon.calledOnce); assert.isTrue(chrome.setPinPreviewIcon.notCalled); }); test('loadend', function() { chrome.handleEvent(mockEvent({ type: 'mozbrowserloadend' })); assert.isFalse(chrome.containerElement.classList.contains('loading')); assert.isTrue(chrome.setSiteIcon.calledOnce); assert.isTrue(chrome.setPinPreviewIcon.calledOnce); assert.equal(0, chrome.setSiteIcon.getCall(0).args.length, 'setSiteIcon passed 0 argument'); }); test('namechanged - does not set when we have a fixed title', function() { chrome._fixedTitle = true; chrome.title.textContent = 'foo'; chrome.handleEvent(mockEvent({ type: '_namechanged' })); assert.equal(chrome.title.textContent, 'foo'); }); suite('error', function() { var app, chrome; setup(function() { app = new AppWindow(cloneConfig(fakeWebSite)); }); test('scrollable chrome without bar', function() { app.config.chrome.bar = false; app.config.chrome.scrollable = true; chrome = new AppChrome(app); chrome.containerElement.classList.add('scrollable'); chrome.handleEvent({ type: 'mozbrowsererror', detail: { type: 'offline' }}); assert.isTrue(chrome.element.classList.contains('maximized')); }); test('not scrollable chrome without bar', function() { app.config.chrome.bar = false; app.config.chrome.scrollable = false; chrome = new AppChrome(app); chrome.handleEvent({ type: 'mozbrowsererror', detail: { type: 'offline' }}); assert.isFalse(chrome.element.classList.contains('maximized')); }); test('scrollable chrome with bar', function() { app.config.chrome.bar = true; app.config.chrome.scrollable = true; chrome = new AppChrome(app); chrome.handleEvent({ type: 'mozbrowsererror', detail: { type: 'fatal' }}); assert.isFalse(chrome.element.classList.contains('maximized')); }); }); }); suite('handleScrollEvent', function() { var app, chrome; setup(function() { app = new AppWindow(cloneConfig(fakeWebSite)); app.config.chrome.bar = true; chrome = new AppChrome(app); this.sinon.stub(chrome, 'expand'); this.sinon.stub(chrome, 'collapse'); }); test('does nothing if is not scrollable', function() { chrome.containerElement.classList.remove('scrollable'); chrome.scrollable = { scrollTop: 100, scrollTopMax: 1 }; chrome.handleEvent(mockEvent({ type: 'scroll'})); assert.isFalse(chrome.expand.called); assert.isFalse(chrome.collapse.called); }); test('collapses when scrollTop > scrollTopMax', function() { chrome.containerElement.classList.add('scrollable'); chrome.scrollable = { scrollTop: 100, scrollTopMax: 55 }; chrome.handleEvent(mockEvent({ type: 'scroll'})); assert.isFalse(chrome.expand.called); assert.isTrue(chrome.collapse.called); }); test('expands when scrollTop > scrollTopMax', function() { chrome.containerElement.classList.add('scrollable'); chrome.scrollable = { scrollTop: 50, scrollTopMax: 55 }; chrome.handleEvent(mockEvent({ type: 'scroll'})); assert.isTrue(chrome.expand.called); assert.isFalse(chrome.collapse.called); }); }); suite('handleScrollAreaChanged', function() { var app, chrome, appVisible, containerHeight, classListAddSpy, event; setup(function() { app = new AppWindow(cloneConfig(fakeWebSite)); app.config.chrome.bar = true; this.sinon.stub(app, 'isVisible', () => appVisible); appVisible = true; chrome = new AppChrome(app); chrome.containerElement.classList.remove('scrollable'); Object.defineProperty(chrome.containerElement, 'clientHeight', { configurable: true, get: () => containerHeight }); classListAddSpy = this.sinon.spy(chrome.containerElement.classList, 'add'); event = { target: app.browser.element, detail: { height: 200 } }; containerHeight = 100; }); test('sets scrollable if browser height > container height', function() { chrome.handleScrollAreaChanged(event); assert.isTrue(chrome.containerElement.classList.contains('scrollable')); }); test('does nothing if browser height <= container height', function() { containerHeight = 200; chrome.handleScrollAreaChanged(event); assert.isFalse(chrome.containerElement.classList.contains('scrollable')); }); test('does nothing if container already scrollable', function() { chrome.containerElement.classList.add('scrollable'); chrome.handleScrollAreaChanged(event); assert.isTrue(classListAddSpy.calledOnce); }); test('does nothing if app not visible', function() { appVisible = false; chrome.handleScrollAreaChanged(event); assert.isFalse(classListAddSpy.called); }); test('does nothing if incorrect target', function() { event.target = null; chrome.handleScrollAreaChanged(event); assert.isFalse(classListAddSpy.called); }); }); suite('setPinDialogCard', function() { setup(function() { this.sinon.stub(chrome, 'setOrigin'); chrome.pinDialog.classList.add('hidden'); chrome.setPinDialogCard(); }); test('displays pin card dialog', function() { assert.isTrue(chrome.setOrigin.called, 'sets the origin'); assert.isFalse(chrome.pinDialog.classList.contains('hidden'), 'shows the pinDialog'); }); test('clicking scrim hides dialog', function() { assert.isFalse(chrome.pinDialog.classList.contains('hidden'), 'pinDialog is shown'); chrome.handleEvent(mockEvent({ type: 'click', target: chrome.pinScrim })); assert.isTrue(chrome.pinDialog.classList.contains('hidden'), 'pinDialog is hidden'); }); }); suite('setOrigin', function() { var DOMAIN, SUBDOMAIN; setup(function() { DOMAIN = 'firefox.org'; SUBDOMAIN = 'test.'; }); test('no subdomains', function() { chrome._currentOrigin = 'http://' + DOMAIN; chrome.setOrigin(); var tld = chrome.originElement.querySelector('.tld'); assert.equal(tld.textContent, DOMAIN); chrome.originElement.removeChild(tld); assert.equal(chrome.originElement.textContent, ''); }); test('with subdomains', function() { chrome._currentOrigin = 'http://' + SUBDOMAIN + DOMAIN; chrome.setOrigin(); var tld = chrome.originElement.querySelector('.tld'); assert.equal(tld.textContent, DOMAIN); chrome.originElement.removeChild(tld); assert.equal(chrome.originElement.textContent, SUBDOMAIN); }); }); suite('URLBar', function() { test('click when collapsed', function() { MockService.mockQueryWith('locked', false); var stubIsBrowser = this.sinon.stub(app, 'isBrowser').returns(true); var stubExpand = this.sinon.stub(chrome, 'expand'); chrome.handleEvent(mockEvent({ type: 'click', target: chrome.title })); assert.isTrue(stubExpand.called); stubExpand.restore(); stubIsBrowser.restore(); }); test('click when expanded', function() { MockService.mockQueryWith('locked', false); chrome.maximize(); var stubDispatchEvent = this.sinon.stub(window, 'dispatchEvent'); chrome.handleEvent(mockEvent({ type: 'click', target: chrome.title })); assert.isTrue(stubDispatchEvent.called); stubDispatchEvent.restore(); }); test('should set the name when created', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); app.name = 'Phone'; var chrome = new AppChrome(app); assert.equal(chrome.title.textContent, 'Phone'); }); test('should update the name when it changes', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); app.name = 'Phone'; var chrome = new AppChrome(app); assert.equal(chrome.title.textContent, 'Phone'); app.name = 'Phone2'; var evt = new CustomEvent('_namechanged'); app.element.dispatchEvent(evt); assert.equal(chrome.title.textContent, 'Phone2'); }); test('should not update the name of homescreen on change', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); app.isHomescreen = true; app.name = 'Home Screen'; var chrome = new AppChrome(app); assert.equal(chrome.title.textContent, ''); app.name = 'Home Screen 2'; var evt = new CustomEvent('_namechanged'); app.element.dispatchEvent(evt); assert.equal(chrome.title.textContent, ''); }); test('localized app is not immediately overridden by titlechange event', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); app.name = 'Phone'; var chrome = new AppChrome(app); chrome.handleEvent({ type: 'mozbrowsertitlechange', detail: 'Do not update' }); assert.equal(chrome.title.textContent, 'Phone'); }); }); suite('mozbrowserlocationchange', function() { var subject = null; var observeSpy, unobserveSpy; setup(function() { var website = new AppWindow(cloneConfig(fakeWebSite)); subject = new AppChrome(website); subject._registerEvents(); }); teardown(function() { subject._unregisterEvents(); }); test('should not do anything on apps with manifests', function() { var app = new AppWindow(fakeAppWithName); var chrome = new AppChrome(app); observeSpy = this.sinon.spy(MockSettingsListener, 'observe'); unobserveSpy = this.sinon.spy(MockSettingsListener, 'unobserve'); chrome._registerEvents(); sinon.assert.calledOnce(observeSpy); sinon.assert.calledWith(observeSpy, PINNING_PREF); sinon.assert.notCalled(unobserveSpy); var evt = new CustomEvent('mozbrowserlocationchange', { detail: 'app://communications.gaiamobile.org/calllog.html' }); chrome.app.element.dispatchEvent(evt); this.sinon.clock.tick(500); chrome._unregisterEvents(); sinon.assert.calledWith(unobserveSpy, PINNING_PREF); sinon.assert.calledOnce(unobserveSpy); }); test('browser start page should always have the same title', function() { var app = new AppWindow(fakeSearchApp); var chrome = new AppChrome(app); var titleEvent = new CustomEvent('mozbrowsertitlechange', { detail: 'Bing' }); chrome.app.element.dispatchEvent(titleEvent); assert.equal(chrome.title.getAttribute('data-l10n-id'), 'search-or-enter-address'); }); test('pins the chrome if the new url is in the scope', function() { var app, chrome; this.sinon.stub(Service, 'request', function() { return { then: function(callback) { callback(true); } }; }); fakeSearchApp.chrome.pinned = true; fakeSearchApp.chrome.scrollable = true; fakeSearchApp.chrome.url = 'http://aaa.com'; app = new AppWindow(fakeSearchApp); this.sinon.stub(app, 'isBrowser').returns(true); chrome = new AppChrome(app); chrome.handleLocationChange(); assert.isTrue(chrome.pinned); assert.isFalse(app.element.classList.contains('collapsible')); }); test('unpins the chrome if the new url is in the scope', function() { var app, chrome; this.sinon.stub(Service, 'request', function() { return { then: function(callback) { callback(false); } }; }); fakeSearchApp.chrome.pinned = true; fakeSearchApp.chrome.scrollable = true; fakeSearchApp.chrome.url = 'http://aaa.com'; app = new AppWindow(fakeSearchApp); this.sinon.stub(app, 'isBrowser').returns(true); chrome = new AppChrome(app); chrome.handleLocationChange(); assert.isFalse(chrome.pinned); assert.isTrue(app.element.classList.contains('collapsible')); }); test('doesnt make it scrollable if the config doesnt say so', function() { fakeSearchApp.chrome.url = 'http://aaa.com'; fakeSearchApp.chrome.scrollable = false; var app = new AppWindow(fakeSearchApp); this.sinon.stub(app, 'isBrowser').returns(true); var chrome = new AppChrome(app); chrome.handleLocationChange(); app.config.url = 'http://test.com'; chrome.handleLocationChange(); assert.isFalse(app.element.classList.contains('collapsible')); }); test('it makes it collapsible again navigating away of a pin', function() { var app, chrome, isPinned; isPinned = true; this.sinon.stub(Service, 'request', function() { return { then: function(callback) { callback(isPinned); } }; }); fakeSearchApp.chrome.pinned = true; fakeSearchApp.chrome.scrollable = true; fakeSearchApp.chrome.url = 'http://aaa.com'; app = new AppWindow(fakeSearchApp); this.sinon.stub(app, 'isBrowser').returns(true); chrome = new AppChrome(app); chrome.handleLocationChange(); assert.isTrue(chrome.pinned); assert.isFalse(app.element.classList.contains('collapsible')); isPinned = false; app.config.url = 'http://blabla.com'; app.config.chrome.scrollable = true; chrome.handleLocationChange(); assert.isTrue(app.element.classList.contains('collapsible')); }); }); suite('Maximized', function() { var subject = null; var cbSpy = null; setup(function() { var app = new AppWindow(cloneConfig(fakeWebSite)); subject = new AppChrome(app); cbSpy = this.sinon.spy(); subject.maximize(cbSpy); }); test('should add the maximized css class on the element', function() { assert.isTrue(subject.element.classList.contains('maximized')); }); test('should trigger the callback after the transition', function() { sinon.assert.notCalled(cbSpy); subject.element.dispatchEvent(new CustomEvent('transitionend')); sinon.assert.calledOnce(cbSpy); }); test('should trigger the callback after a safety timeout', function() { sinon.assert.notCalled(cbSpy); this.sinon.clock.tick(500); sinon.assert.calledOnce(cbSpy); }); test('collapse should remove the maximized css class', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); var subject = new AppChrome(app); subject.element.classList.add('maximized'); subject.collapse(); assert.isFalse(subject.element.classList.contains('maximized')); }); test('rocketbar-overlayclosed should collapse the urlbar', function() { var app = new AppWindow(cloneConfig(fakeWebSite)); var subject = new AppChrome(app); subject.maximize(); window.dispatchEvent(new CustomEvent('rocketbar-overlayclosed')); assert.isFalse(subject.element.classList.contains('maximized')); }); }); suite('Theme-Color', function() { var app, chrome, stubRequestAnimationFrame, appPublishStub; setup(function() { this.sinon.clock.restore(); app = new AppWindow(cloneConfig(fakeWebSite)); chrome = new AppChrome(app); stubRequestAnimationFrame = this.sinon.stub(window, 'requestAnimationFrame').yieldsAsync(); appPublishStub = this.sinon.stub(app, 'publish'); }); teardown(function(done) { // setThemeColor triggers a rAF loop that doesn't finish until // it gets a transitionend event. Some tests dispatch this event // but others don't so we dispatch it again here in case it hasn't // been dispatched yet. // // If we fail to do this, the rAF loop triggered by a previous test // might still be running when we start the next test which will // mean we *sometimes* get surprising values for // stubRequestAnimationFrame.callCount. // // Note that here we only make sure the rAF loop on |chrome| has // finished. Individual tests are responsible for firing a // transitionend event at the element of any additional AppChrome // objects they create. chrome.element.dispatchEvent(fadeTransitionEndEvent); // To ensure the transitionend event has been processed, wait for a // *real* requestAnimationFrame tick. window.requestAnimationFrame.restore(); // Make sure we pass null to |done| otherwise it will complain that // its argument is not an Error object. window.requestAnimationFrame(done.bind(null, null)); }); test('metachange already set', function() { app.themeColor = 'orange'; chrome = new AppChrome(app); assert.equal(chrome.element.style.backgroundColor, 'orange'); }); test('metachange added', function() { chrome.handleEvent({ type: 'mozbrowsermetachange', detail: { name: 'theme-color', type: 'added', content: 'orange' } }); assert.equal(chrome.element.style.backgroundColor, 'orange'); }); test('metachange removed', function() { chrome.handleEvent({ type: 'mozbrowsermetachange', detail: { name: 'theme-color', type: 'removed' } }); assert.equal(chrome.element.style.backgroundColor, ''); assert.isTrue(app.element.classList.contains('light')); }); test('metachange changed', function() { chrome.handleEvent({ type: 'mozbrowsermetachange', detail: { name: 'theme-color', type: 'changed', content: 'red' } }); assert.equal(chrome.element.style.backgroundColor, 'red'); }); test('theme resets on navigation', function() { chrome.setThemeColor('orange'); chrome.handleEvent(mockEvent({type: 'mozbrowserloadstart'})); chrome.handleEvent(mockEvent({type: 'mozbrowserloadend'})); assert.equal(chrome.element.style.backgroundColor, ''); }); test('dark color have light icons', function(done) { var initiallyLight = app.element.classList.contains('light'); chrome.setThemeColor('black'); window.setTimeout(function() { chrome.element.dispatchEvent(fadeTransitionEndEvent); assert.isTrue(stubRequestAnimationFrame.called); assert.isFalse(app.element.classList.contains('light')); assert.isFalse(chrome.useLightTheming()); sinon.assert.callCount(appPublishStub.withArgs('titlestatechanged'), initiallyLight ? 1 : 0); done(); }, 0); }); test('light color have dark icons', function(done) { var initiallyLight = app.element.classList.contains('light'); chrome.setThemeColor('white'); window.setTimeout(function() { chrome.element.dispatchEvent(fadeTransitionEndEvent); assert.isTrue(stubRequestAnimationFrame.called); assert.isTrue(app.element.classList.contains('light')); assert.isTrue(chrome.useLightTheming()); sinon.assert.callCount(appPublishStub.withArgs('titlestatechanged'), initiallyLight ? 0 : 1); done(); }, 0); }); test('popup window will use rear window color theme', function(done) { var popup = new PopupWindow(cloneConfig(fakeWebSite)); var popupChrome = new AppChrome(popup); this.sinon.stub(popup, 'getBottomMostWindow').returns(app); chrome.setThemeColor('black'); popupChrome.setThemeColor('white'); popup.appChrome = popupChrome; app.appChrome = chrome; window.setTimeout(function() { chrome.element.dispatchEvent(fadeTransitionEndEvent); assert.isTrue(stubRequestAnimationFrame.called); assert.equal(chrome.useLightTheming(), popupChrome.useLightTheming()); assert.equal(app.themeColor, 'black'); assert.equal(popup.themeColor, 'black'); sinon.assert.calledOnce(appPublishStub.withArgs('titlestatechanged')); // End popup rAF look so it doesn't interfere with other tests popupChrome.element.dispatchEvent(fadeTransitionEndEvent); done(); }, 0); }); test('popup window should not be collapsible', function() { var popup = new PopupWindow(cloneConfig(fakeAppWithName)); this.sinon.stub(popup, 'isBrowser').returns(true); this.sinon.stub(popup, 'getBottomMostWindow').returns(app); var popupChrome = new AppChrome(popup); this.sinon.stub(Service, 'request').returns({ then: function(cb) { cb(false); } }); popupChrome.handleLocationChange(); assert.isFalse(popupChrome.pinned); assert.isFalse(popup.element.classList.contains('collapsible')); }); test('browser scrollable background is black', function() { assert.equal(chrome.scrollable.style.backgroundColor, ''); chrome.setThemeColor('black'); assert.equal(chrome.scrollable.style.backgroundColor, 'black'); }); test('should stop requesting frames when transition ends', function(done) { chrome.setThemeColor('white'); chrome.element.dispatchEvent(fadeTransitionEndEvent); window.setTimeout(function() { sinon.assert.calledOnce(stubRequestAnimationFrame); done(); }, 0); }); test('should ignore unrelated transition events', function(done) { chrome.setThemeColor('white'); window.setTimeout(function() { var previousCallCount = stubRequestAnimationFrame.callCount; var transformEvent = new CustomEvent('transitionend'); transformEvent.propertyName = 'transform'; chrome.element.dispatchEvent(transformEvent); window.setTimeout(function() { assert.isTrue(stubRequestAnimationFrame.callCount > previousCallCount, 'requestAnimationFrame continues to be called after ' + 'an unrelated transition event'); done(); }, 0); }, 0); }); test('homescreen scrollable background is unset', function() { app.isHomescreen = true; var chrome = new AppChrome(app); assert.equal(chrome.scrollable.style.backgroundColor, ''); chrome.setThemeColor('black'); assert.equal(chrome.scrollable.style.backgroundColor, ''); }); test('does not set for private windows', function() { app = new AppWindow(cloneConfig(fakeWebSite)); this.sinon.stub(app, 'isPrivateBrowser').returns(true); chrome = new AppChrome(app); assert.equal(chrome.scrollable.style.backgroundColor, ''); chrome.setThemeColor('black'); assert.equal(chrome.scrollable.style.backgroundColor, 'rgb(57, 46, 84)'); }); }); suite('Search request', function() { var stubDispatch; setup(function() { stubDispatch = this.sinon.stub(window, 'dispatchEvent'); }); test('When screen is unlocked, dispatch the request.', function() { MockService.mockQueryWith('locked', false); this.sinon.stub(chrome, 'isMaximized').returns(true); chrome.handleEvent(mockEvent({ type: 'click', target: chrome.title })); assert.isTrue(stubDispatch.called); }); test('When a contextmenu is shown, do not dispatch.', function() { MockService.mockQueryWith('locked', false); this.sinon.stub(chrome, 'isMaximized').returns(true); this.sinon.stub(chrome.app.contextmenu, 'isShown').returns(true); chrome.handleEvent(mockEvent({ type: 'click', target: chrome.title })); assert.isFalse(stubDispatch.called); }); test('When screen is locked, do not dispatch the event.', function() { MockService.mockQueryWith('locked', true); this.sinon.stub(chrome, 'isMaximized').returns(true); chrome.handleEvent(mockEvent({ type: 'click', target: chrome.title })); assert.isFalse(stubDispatch.called); }); }); suite('titlestatechanged', function() { test('scroll event - active app', function() { var app = new AppWindow(fakeAppMaximized); var chrome = new AppChrome(app); var appPublishStub = this.sinon.stub(app, 'publish'); this.sinon.stub(app, 'isActive').returns(true); chrome.handleEvent(mockEvent({ type: 'scroll' })); assert.isTrue(appPublishStub.called); assert.isTrue(appPublishStub.calledWith('titlestatechanged')); }); test('scroll event - inactive app', function() { var app = new AppWindow(fakeAppMaximized); var chrome = new AppChrome(app); var appPublishStub = this.sinon.stub(app, 'publish'); this.sinon.stub(app, 'isActive').returns(false); chrome.handleEvent(mockEvent({ type: 'scroll' })); assert.isTrue(appPublishStub.notCalled); }); test('set transparent color', function() { var appPublishStub = this.sinon.stub(app, 'publish'); chrome.setThemeColor('transparent'); assert.isTrue(appPublishStub.called); assert.isTrue(appPublishStub.calledWith('titlestatechanged')); }); test('unset color', function() { var appPublishStub = this.sinon.stub(app, 'publish'); chrome.setThemeColor(''); assert.isTrue(appPublishStub.called); assert.isTrue(appPublishStub.calledWith('titlestatechanged')); }); }); suite('reConfig', function() { test('removes the search-app class if not the search app', function() { var app = new AppWindow(fakeSearchApp); var chrome = new AppChrome(app); assert.isTrue(chrome.app.element.classList.contains('search-app')); chrome.app.config.manifest = null; chrome.reConfig(); assert.isFalse(chrome.app.element.classList.contains('search-app')); }); test('sets the site icon', function() { var app = new AppWindow(fakeSearchApp); var chrome = new AppChrome(app); assert.isTrue(chrome.setSiteIcon.called); }); }); suite('transition events', function() { var app, chrome; setup(function() { app = new AppWindow(fakeAppMaximized); chrome = new AppChrome(app); this.sinon.stub(app, 'publish'); this.sinon.stub(app, 'isActive').returns(true); this.sinon.stub(chrome, 'isMaximized').returns(false); for (var i = 0; i < 10; i++) { chrome.handleEvent(mockEvent({ type: 'scroll' })); } }); test('should not publish when a control transition ends', function() { // Can not fire transitionend events on disabled buttons. chrome.reloadButton.disabled = false; chrome.reloadButton.dispatchEvent(new CustomEvent('transitionend')); sinon.assert.notCalled(app.publish.withArgs('chromecollapsed')); }); test('should publish when the element transition ends', function() { chrome.element.dispatchEvent(new CustomEvent('transitionend')); sinon.assert.calledOnce(app.publish.withArgs('chromecollapsed')); }); test('publishes chromeexpanded when transition ends', function() { chrome.isMaximized.returns(true); chrome.element.dispatchEvent(new CustomEvent('transitionend')); sinon.assert.calledOnce(app.publish.withArgs('chromeexpanded')); }); test('should only publish once', function() { chrome.element.dispatchEvent(new CustomEvent('transitionend')); this.sinon.clock.tick(250); sinon.assert.calledOnce(app.publish.withArgs('chromecollapsed')); }); }); suite('Pinning the web', function() { setup(function() { [chrome.siteIcon, chrome.pinScrim].forEach(function(element) { this.sinon.stub(element, 'addEventListener'); this.sinon.stub(element, 'removeEventListener'); }.bind(this)); }); // Skip test since we are disabling pinning door hanger in 2.5 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1207710 // test('adds listeners when the setting is enabled', function() { // MockSettingsListener.mTriggerCallback(PINNING_PREF, true); // assert.isTrue(chrome.siteIcon.addEventListener.calledWith('click')); // assert.isTrue(chrome.pinScrim.addEventListener.calledWith('click')); // }); // Skip test since we are disabling pinning door hanger in 2.5 // See https://bugzilla.mozilla.org/show_bug.cgi?id=1207710 // test('removes listeners when the setting is enabled', function() { // MockSettingsListener.mTriggerCallback(PINNING_PREF, false); // assert.isFalse(chrome.siteIcon.addEventListener.calledWith('click')); // assert.isFalse(chrome.pinScrim.addEventListener.calledWith('click')); // }); }); suite('setSiteIcon', function() { var fakeIconURI = 'data://someimage'; var getIconPromise; var combinedChrome; setup(function() { var app = new AppWindow(cloneConfig(fakeWebSite)); combinedChrome = new AppChrome(app); combinedChrome.setSiteIcon.restore(); getIconPromise = new MockPromise(); this.sinon.stub(combinedChrome.app, 'getSiteIconUrl') .returns(getIconPromise); }); test('asks app for url when no argument is provided', function() { assert.ok(combinedChrome.useCombinedChrome()); combinedChrome.setSiteIcon(); getIconPromise.mFulfillToValue({originalUrl: fakeIconURI, blob: {}}); assert.equal(combinedChrome._currentIconUrl, fakeIconURI); }); test('failure to get icon', function() { // set a default combinedChrome.siteIcon.style.backgroundImage = `url(${fakeIconURI})`; combinedChrome.setSiteIcon(); getIconPromise.mRejectToError(); assert.equal(combinedChrome.siteIcon.style.backgroundImage, ''); }); test('has no effect for private browsers', function() { this.sinon.stub(combinedChrome.app, 'isPrivateBrowser').returns(true); var siteIcon = combinedChrome.siteIcon; var origSiteIcon = siteIcon && siteIcon.style.backgroundImage; var origClassName = siteIcon && siteIcon.className; combinedChrome.setSiteIcon(fakeIconURI); var newSiteIcon = siteIcon && siteIcon.style.backgroundImage; var newClassName = siteIcon && siteIcon.className; assert.equal(origSiteIcon, newSiteIcon); assert.equal(origClassName, newClassName); assert.ok(!combinedChrome.app.getSiteIconUrl.called); }); // test('click and pref disabled', function() { // MockSettingsListener.mTriggerCallback(PINNING_PREF, false); // this.sinon.stub(combinedChrome, 'setPinDialogCard'); // this.sinon.stub(combinedChrome.app, 'isBrowser').returns(true); // this.sinon.stub(combinedChrome, 'isMaximized').returns(true); // combinedChrome.siteIcon.dispatchEvent(new CustomEvent('click')); // assert.isFalse(combinedChrome.setPinDialogCard.called); // }); // test('click and pref enabled', function() { // MockSettingsListener.mTriggerCallback(PINNING_PREF, true); // this.sinon.stub(combinedChrome, 'setPinDialogCard'); // this.sinon.stub(combinedChrome.app, 'isBrowser').returns(true); // this.sinon.stub(combinedChrome, 'isMaximized').returns(true); // combinedChrome.siteIcon.dispatchEvent(new CustomEvent('click')); // assert.isTrue(combinedChrome.setPinDialogCard.called); // }); test('click, but no browser', function() { MockSettingsListener.mTriggerCallback(PINNING_PREF, true); this.sinon.stub(combinedChrome, 'setPinDialogCard'); this.sinon.stub(combinedChrome.app, 'isBrowser').returns(false); this.sinon.stub(combinedChrome, 'isMaximized').returns(true); combinedChrome.siteIcon.dispatchEvent(new CustomEvent('click')); assert.isFalse(combinedChrome.setPinDialogCard.called); }); test('click, but loading', function() { MockSettingsListener.mTriggerCallback(PINNING_PREF, true); this.sinon.stub(combinedChrome, 'setPinDialogCard'); this.sinon.stub(combinedChrome.app, 'isBrowser').returns(true); this.sinon.stub(combinedChrome, 'isMaximized').returns(true); combinedChrome.app.loading = true; combinedChrome.siteIcon.dispatchEvent(new CustomEvent('click')); assert.isFalse(combinedChrome.setPinDialogCard.called); combinedChrome.app.loading = false; }); test('click, but not maximized', function() { MockSettingsListener.mTriggerCallback(PINNING_PREF, true); this.sinon.stub(combinedChrome, 'setPinDialogCard'); this.sinon.stub(combinedChrome.app, 'isBrowser').returns(true); this.sinon.stub(combinedChrome, 'isMaximized').returns(false); combinedChrome.siteIcon.dispatchEvent(new CustomEvent('click')); assert.isFalse(combinedChrome.setPinDialogCard.called); }); }); suite('Pin page', function() { var chrome, requestStub; setup(function() { var app = new AppWindow(cloneConfig(fakeWebSite)); chrome = new AppChrome(app); requestStub = this.sinon.stub(Service, 'request').returns( new Promise(function() {})); }); teardown(function() { requestStub.restore(); }); test('Pin page', function() { chrome.pinPage(); assert.isTrue(Service.request.calledWith('Places:setPinned', fakeWebSite.url)); }); }); suite('Unpin page', function() { var chrome, requestStub; setup(function() { var app = new AppWindow(cloneConfig(fakeWebSite)); chrome = new AppChrome(app); requestStub = this.sinon.stub(Service, 'request').returns( new Promise(function() {})); }); teardown(function() { requestStub.restore(); }); test('Unpin page', function() { chrome.unpinPage(); assert.isTrue(Service.request.calledWith('Places:setPinned', fakeWebSite.url)); }); }); suite('getSiteUrl', function() { test('no webManifest', function() { app = new AppWindow(cloneConfig(fakeWebSite)); assert.ok(chrome.getSiteUrl(), 'http://google.com/index.html'); }); test('webManifest', function() { app = new AppWindow(cloneConfig(fakeWebApp)); assert.ok(chrome.getSiteUrl(), 'http://example.com/index.html'); }); test('webManifest with start_url', function() { var config = cloneConfig(fakeWebApp); config.webManifest.start_url = '/start.html'; config.url = 'http://example.com/some/path/foo.html'; app = new AppWindow(config); assert.ok(chrome.getSiteUrl(), 'http://example.com/start.html'); }); }); suite('Pin site without manifest', function() { setup(function() { this.sinon.stub(chrome, 'getSiteUrl'). returns('http://google.com/'); this.sinon.stub(IconsHelper, 'getIcon'). returns(Promise.resolve('http://google.com/favicon.ico')); this.sinon.stub(BookmarksDatabase, 'put', () => { chrome.handleEvent({ type: 'pins-scopechange', detail: { type: 'added', url: 'http://google.com/index.html', name: 'google.com' } }); return Promise.resolve(); }); this.sinon.stub(chrome.systemBanner, 'show'); this.sinon.spy(chrome, 'pin'); this.sinon.stub(chrome.app, 'inScope').returns(true); this.sinon.stub(chrome.app, 'matchesOriginAndName').returns(true); }); test('Browser chrome collapsed and bookmark saved', function() { chrome.element.classList.add('maximized'); chrome.pinDialog.classList.remove('hidden'); chrome.handleEvent( mockEvent({ type: 'click', target: chrome.pinButton })); assert.isTrue(IconsHelper.getIcon.called); process.nextTick(function() { assert.isTrue(BookmarksDatabase.put.calledWithMatch({ type: 'url', iconable: false, frecency: 1, pinned: true, pinnedFrom: 'http://google.com/index.html', id: 'http://google.com/', name: 'google.com', url: 'http://google.com/', icon: 'http://google.com/favicon.ico' }, 'http://google.com/')); assert(chrome.systemBanner.show.called); }); }); }); suite('Pin site with manifest', function() { var chrome; var putBookmarkPromise; var getIconPromise; setup(function() { var app = new AppWindow(cloneConfig(fakeWebApp)); this.sinon.stub(app, 'inScope').returns(true); chrome = new AppChrome(app); this.sinon.stub(chrome, 'getSiteUrl'). returns('http://example.com/'); getIconPromise = new MockPromise(); this.sinon.stub(IconsHelper, 'getIcon') .returns(getIconPromise); putBookmarkPromise = new MockPromise(); this.sinon.stub(BookmarksDatabase, 'put') .returns(putBookmarkPromise); this.sinon.stub(chrome.systemBanner, 'show'); }); test('Browser chrome collapsed and bookmark saved no scope', function() { chrome.element.classList.add('maximized'); chrome.pinDialog.classList.remove('hidden'); this.sinon.stub(chrome.app, 'matchesOriginAndName').returns(true); chrome.handleEvent(mockEvent({ type: 'click', target: chrome.pinButton })); getIconPromise.mFulfillToValue('http://example.com/favicon.ico'); putBookmarkPromise.mFulfillToValue(); chrome.handleEvent(mockEvent({ type: 'pins-scopechange', detail: { type: 'added', url: 'http://example.com/index.html', name: 'Example' } })); assert.isTrue(IconsHelper.getIcon.called); assert.isTrue(BookmarksDatabase.put.calledWithMatch({ type: 'url', iconable: false, frecency: 1, pinned: true, pinnedFrom: 'http://example.com/index.html', id: 'http://example.com/', url: 'http://example.com/', webManifestUrl: 'http://example.com/manifest.webmanifest', webManifest: { name: 'Example Web App', short_name: 'Example' }, name: 'Example', icon: 'http://example.com/favicon.ico' }, 'http://example.com/')); assert(chrome.systemBanner.show.called); }); }); suite('Unpin site', function() { var removeBookmarkPromise; setup(function() { removeBookmarkPromise = new MockPromise(); this.sinon.stub(BookmarksDatabase, 'remove') .returns(removeBookmarkPromise); this.sinon.stub(chrome, 'getSiteUrl').returns('http://example.com/'); this.sinon.stub(chrome.systemBanner, 'show'); this.sinon.stub(chrome.app, 'inScope').returns(true); chrome.pinned = true; }); test('Dialog reflects pinned state', function() { this.sinon.stub(chrome, 'unpinSite'); chrome.setPinDialogCard(); chrome.handleEvent( mockEvent({ type: 'click', target: chrome.pinButton })); assert.isTrue(chrome.unpinSite.calledOnce); }); test('Site is removed and unpinned', function() { chrome.unpinSite(); removeBookmarkPromise.mFulfillToValue(); chrome.handleEvent({ type: 'pins-scopechange', detail: { url: 'http://example.com', name: 'Example', action: 'remove' } }); assert.isTrue(BookmarksDatabase.remove .calledWithMatch('http://example.com')); assert.isTrue(chrome.systemBanner.show.calledOnce); }); }); suite('handleBookmarksScopeChange', function() { var evt, isInScope, matches; setup(function() { evt = { detail: { action: 'add', url: 'http://example.com/bla/index.html', scope: '/bla', name: 'Example' } }; var app = new AppWindow(cloneConfig(fakeWebApp)); chrome = new AppChrome(app); this.sinon.stub(chrome.app, 'inScope', function() { return isInScope; }); this.sinon.stub(chrome.app, 'matchesOriginAndName', function() { return matches; }); this.sinon.stub(chrome, 'pin'); this.sinon.stub(chrome, 'unpin'); }); test('does nothing if scope provided and not in the scope', function() { isInScope = false; chrome.handleBookmarksScopeChange(evt); assert(chrome.pin.notCalled); }); test('no scope and does not match origin and name', function() { matches = false; evt.detail.scope = false; chrome.handleBookmarksScopeChange(evt); assert(chrome.pin.notCalled); }); test('pins when scope added and is inScope', function() { isInScope = true; chrome.handleBookmarksScopeChange(evt); assert(chrome.pin.called); }); test('pins when no inScope but matches origin and name', function() { matches = true; evt.detail.scope = false; chrome.handleBookmarksScopeChange(evt); assert(chrome.pin.called); }); test('unpins when removed and inScope', function() { isInScope = true; evt.detail.action = 'remove'; chrome.handleBookmarksScopeChange(evt); assert(chrome.unpin.called); }); }); suite('Default icon', function() { var chrome; var getIconPromise; setup(function() { var app = new AppWindow(cloneConfig(fakeWebSite)); chrome = new AppChrome(app); getIconPromise = new MockPromise(); this.sinon.stub(chrome.app, 'getSiteIconUrl').returns(getIconPromise); chrome.app.config.url = 'http://origin1/'; chrome.handleEvent(mockEvent({ type: '_locationchange' })); chrome.setSiteIcon.reset(); }); test('Icon is not set to default when same origin', function() { chrome.handleEvent(mockEvent({ type: '_locationchange' })); assert.isFalse(chrome.setSiteIcon.called); }); test('Icon is set to default when same origin', function() { chrome.app.config.url = 'http://origin2/'; chrome.handleEvent({ type: '_locationchange' }); assert.isTrue(chrome.setSiteIcon.calledOnce); assert.equal(0, chrome.setSiteIcon.getCall(0).args.length); }); }); });