diff --git a/apps/system/index.html b/apps/system/index.html index b2b729bb429d..60e88aaac668 100644 --- a/apps/system/index.html +++ b/apps/system/index.html @@ -373,6 +373,7 @@ + diff --git a/apps/system/js/activity_window.js b/apps/system/js/activity_window.js index 27ffd5d38277..6952b63b0971 100644 --- a/apps/system/js/activity_window.js +++ b/apps/system/js/activity_window.js @@ -88,6 +88,8 @@ ActivityWindow.prototype = Object.create(AppWindow.prototype); + ActivityWindow.prototype.constructor = ActivityWindow; + ActivityWindow.prototype.eventPrefix = 'activity'; ActivityWindow.prototype.CLASS_NAME = 'ActivityWindow'; @@ -166,11 +168,6 @@ _id++; return '
' + - '
' + - '
' + - '
' + - '
' + - '
' + '
' + '
' + '
' + @@ -184,13 +181,11 @@ 'valueSelector': window.ValueSelector, 'authDialog': window.AppAuthenticationDialog, 'contextmenu': window.BrowserContextMenu, - 'childWindowFactory': window.ChildWindowFactory + 'childWindowFactory': window.ChildWindowFactory, + 'statusbar': window.AppStatusbar }; - ActivityWindow.REGISTERED_EVENTS = - ['mozbrowserclose', 'mozbrowsererror', 'mozbrowservisibilitychange', - 'mozbrowserloadend', 'mozbrowseractivitydone', 'mozbrowserloadstart', - '_localized']; + ActivityWindow.REGISTERED_EVENTS = AppWindow.REGISTERED_EVENTS; ActivityWindow.prototype._handle_mozbrowseractivitydone = function aw__handle_mozbrowseractivitydone() { diff --git a/apps/system/js/app_statusbar.js b/apps/system/js/app_statusbar.js new file mode 100644 index 000000000000..d67b45be9706 --- /dev/null +++ b/apps/system/js/app_statusbar.js @@ -0,0 +1,173 @@ +/* global BaseUI, TouchForwarder */ + +'use strict'; + +(function(exports) { + var AppStatusbar = function(app) { + this.app = app; + this.containerElement = app.element; + this.render(); + this._fetchAllElements(); + this._touchForwarder = new TouchForwarder(); + this._touchForwarder.destination = this.app.browser.element; + }; + + AppStatusbar.prototype = Object.create(BaseUI.prototype); + AppStatusbar.prototype.constructor = AppStatusbar; + AppStatusbar.prototype.EVENT_PREFIX = 'appstatusbar'; + AppStatusbar.prototype.DEBUG = false; + AppStatusbar.prototype.view = function() { + return '
' + + '
' + + '
' + + '
' + + '
'; + }; + AppStatusbar.prototype.RELEASE_TIMEOUT = 5000; + AppStatusbar.prototype.screen = document.getElementById('screen'); + + AppStatusbar.prototype._fetchAllElements = function(first_argument) { + this.titleBar = this.containerElement.querySelector('.titlebar'); + }; + + AppStatusbar.prototype.handleStatusbarTouch = function(evt, barHeight) { + this.app.debug('preprocessing touch event...', evt.type); + var touch; + var cacheHeight = barHeight; + if (!this.chromeBar) { + // Because app chrome is loaded after us. + this.chromeBar = this.app.element.querySelector('.chrome'); + } + if (this._dontStopEvent) { + return; + } + // If system is at fullscreen mode, let utility tray to handle the event. + if (!this.app || (!this.app.isFullScreen() && !document.mozFullScreen)) { + return; + } + this.app.debug('processing touch event...', evt.type); + + evt.stopImmediatePropagation(); + evt.preventDefault(); + switch (evt.type) { + case 'touchstart': + clearTimeout(this._releaseTimeout); + this._touchStart = evt; + this._shouldForwardTap = true; + + touch = evt.changedTouches[0]; + this._startX = touch.clientX; + this._startY = touch.clientY; + + this.chromeBar.style.transition = 'transform'; + this.titleBar.style.transition = 'transform'; + break; + + case 'touchmove': + touch = evt.touches[0]; + var height = cacheHeight; + var deltaX = touch.clientX - this._startX; + var deltaY = touch.clientY - this._startY; + + if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { + this._shouldForwardTap = false; + } + + var translate = Math.min(deltaY, height); + var heightThreshold = height; + + if (this.app && this.app.isFullScreen() && this.app.config.chrome && + this.app.config.chrome.navigation) { + translate = Math.min(deltaY, this.app.appChrome.height); + heightThreshold = this.app.appChrome.height; + + this.titleBar.style.transform = 'translateY(calc(' + + (translate - this.app.appChrome.height) + 'px)'; + } else { + this.titleBar.style.transform = + 'translateY(calc(' + translate + 'px - 100%)'; + } + this.chromeBar.style.transform = + 'translateY(calc(' + translate + 'px - 100%)'; + + this.app.debug(translate, heightThreshold); + if (translate >= heightThreshold) { + if (this._touchStart) { + this._touchForwarder.forward(this._touchStart); + this._touchStart = null; + } + this._touchForwarder.forward(evt); + } + break; + + case 'touchend': + clearTimeout(this._releaseTimeout); + + if (this._touchStart) { + if (this._shouldForwardTap) { + this._touchForwarder.forward(this._touchStart); + this._touchForwarder.forward(evt); + this._touchStart = null; + } + this._releaseBar(); + } else { + this._dontStopEvent = true; + this._touchForwarder.forward(evt); + this._releaseAfterTimeout(); + } + break; + } + }; + + AppStatusbar.prototype._releaseBar = function() { + this.app.debug('releasing statusbar'); + this._dontStopEvent = false; + var titleBar = this.titleBar; + var chromeBar = this.chromeBar; + + chromeBar.classList.remove('dragged'); + chromeBar.style.transform = ''; + chromeBar.style.transition = ''; + + titleBar.classList.remove('dragged'); + titleBar.style.transform = ''; + titleBar.style.transition = ''; + + this.screen.classList.remove('minimized-tray'); + + clearTimeout(this._releaseTimeout); + this._releaseTimeout = null; + }; + + AppStatusbar.prototype._releaseAfterTimeout = function() { + this.app.debug('stay until 5s'); + this.screen.classList.add('minimized-tray'); + var chromeBar = this.chromeBar; + var titleBar = this.titleBar; + + var self = this; + titleBar.style.transform = ''; + titleBar.style.transition = ''; + titleBar.classList.add('dragged'); + + chromeBar.style.transform = ''; + chromeBar.style.transition = ''; + chromeBar.classList.add('dragged'); + + self._releaseTimeout = setTimeout(function() { + self._releaseBar(); + window.removeEventListener('touchstart', closeOnTap); + }, this.RELEASE_TIMEOUT); + + function closeOnTap(evt) { + if (evt.target != self._touchForwarder.destination) { + return; + } + window.removeEventListener('touchstart', closeOnTap); + self.app.debug('user interaction, will release statusbar.'); + self._releaseBar(titleBar); + } + window.addEventListener('touchstart', closeOnTap); + }; + exports.AppStatusbar = AppStatusbar; +}(window)); diff --git a/apps/system/js/app_window.js b/apps/system/js/app_window.js index a1d193c8af97..aea515049ab3 100644 --- a/apps/system/js/app_window.js +++ b/apps/system/js/app_window.js @@ -520,11 +520,6 @@ return '
' + - '
' + - '
' + - '
' + - '
' + - '
' + '
' + '
' + '
' + @@ -733,6 +728,7 @@ 'authDialog': window.AppAuthenticationDialog, 'contextmenu': window.BrowserContextMenu, 'childWindowFactory': window.ChildWindowFactory, + 'statusbar': window.AppStatusbar }; AppWindow.prototype.openAnimation = 'enlarge'; @@ -2193,5 +2189,16 @@ } this.setVisible(false); }; + + /** + * Statusbar will bypass touch event to us via this method + * @param {Object} evt Touch event object + * @param {Number} barHeight The height of the statusbar + */ + AppWindow.prototype.handleStatusbarTouch = function(evt, barHeight) { + if (this.statusbar) { + this.statusbar.handleStatusbarTouch(evt, barHeight); + } + }; exports.AppWindow = AppWindow; }(window)); diff --git a/apps/system/js/attention_window.js b/apps/system/js/attention_window.js index c0639f1e89cc..d030694911b3 100644 --- a/apps/system/js/attention_window.js +++ b/apps/system/js/attention_window.js @@ -90,10 +90,6 @@ this.debug('intance id: ' + this.instanceID); return '
' + - '
' + - '
' + - '
' + - '
' + '
' + '
' + '
'; @@ -103,7 +99,8 @@ 'transitionController': window.AppTransitionController, 'modalDialog': window.AppModalDialog, 'authDialog': window.AppAuthenticationDialog, - 'attentionToaster': window.AttentionToaster + 'attentionToaster': window.AttentionToaster, + 'stautsbar': window.AppStatusbar }; AttentionWindow.REGISTERED_EVENTS = diff --git a/apps/system/js/hierarchy_manager.js b/apps/system/js/hierarchy_manager.js index 72b1f32b3cd2..1e3fee3f693d 100644 --- a/apps/system/js/hierarchy_manager.js +++ b/apps/system/js/hierarchy_manager.js @@ -8,6 +8,9 @@ 'registerHierarchy', 'unregisterHierarchy' ]; + HierarchyManager.STATES = [ + 'getTopMostWindow' + ]; BaseModule.create(HierarchyManager, { name: 'HierarchyManager', EVENT_PREFIX: 'hierachy', diff --git a/apps/system/js/homescreen_window.js b/apps/system/js/homescreen_window.js index d1abf0487476..dd70e2940bd4 100644 --- a/apps/system/js/homescreen_window.js +++ b/apps/system/js/homescreen_window.js @@ -70,6 +70,8 @@ HomescreenWindow.prototype = Object.create(AppWindow.prototype); + HomescreenWindow.prototype.constructor = HomescreenWindow; + HomescreenWindow.prototype._DEBUG = false; HomescreenWindow.prototype.CLASS_NAME = 'HomescreenWindow'; @@ -102,17 +104,15 @@ this.isHomescreen = true; }; - HomescreenWindow.REGISTERED_EVENTS = - ['_opening', '_localized', 'mozbrowserclose', 'mozbrowsererror', - 'mozbrowservisibilitychange', 'mozbrowserloadend', '_orientationchange', - '_focus']; + HomescreenWindow.REGISTERED_EVENTS = AppWindow.REGISTERED_EVENTS; HomescreenWindow.SUB_COMPONENTS = { 'transitionController': window.AppTransitionController, 'modalDialog': window.AppModalDialog, 'valueSelector': window.ValueSelector, 'authDialog': window.AppAuthenticationDialog, - 'childWindowFactory': window.ChildWindowFactory + 'childWindowFactory': window.ChildWindowFactory, + 'statusbar': window.AppStatusbar }; HomescreenWindow.prototype.openAnimation = 'zoom-out'; @@ -165,11 +165,6 @@ HomescreenWindow.prototype.view = function hw_view() { return '
' + - '
' + - '
' + - '
' + - '
' + - '
' + '
' + '
' + '
'; diff --git a/apps/system/js/lockscreen_window.js b/apps/system/js/lockscreen_window.js index 2aade874b137..67c8032df8de 100644 --- a/apps/system/js/lockscreen_window.js +++ b/apps/system/js/lockscreen_window.js @@ -44,6 +44,15 @@ */ LockScreenWindow.prototype = Object.create(AppWindow.prototype); + LockScreenWindow.prototype.constructor = LockScreenWindow; + + LockScreenWindow.SUB_COMPONENTS = { + 'transitionController': window.AppTransitionController, + 'statusbar': window.AppStatusbar + }; + + LockScreenWindow.REGISTERED_EVENTS = AppWindow.REGISTERED_EVENTS; + /** * We still need this before we put the lockreen inside an iframe. * diff --git a/apps/system/js/popup_window.js b/apps/system/js/popup_window.js index 5afc9e2680d6..673c75cccbb8 100644 --- a/apps/system/js/popup_window.js +++ b/apps/system/js/popup_window.js @@ -41,7 +41,8 @@ 'valueSelector': window.ValueSelector, 'authDialog': window.AppAuthenticationDialog, 'contextmenu': window.BrowserContextMenu, - 'childWindowFactory': window.ChildWindowFactory + 'childWindowFactory': window.ChildWindowFactory, + 'statusbar': window.AppStatusbar }; /** diff --git a/apps/system/js/statusbar.js b/apps/system/js/statusbar.js index caae2d59b249..364965749377 100644 --- a/apps/system/js/statusbar.js +++ b/apps/system/js/statusbar.js @@ -14,7 +14,7 @@ limitations under the License. */ -/*global Clock, SettingsListener, TouchForwarder, FtuLauncher, MobileOperator, +/*global Clock, SettingsListener, FtuLauncher, MobileOperator, SIMSlotManager, Service, Bluetooth, UtilityTray, nfcManager, layoutManager */ @@ -649,14 +649,6 @@ var StatusBar = { ); }, - _startX: null, - _startY: null, - _releaseTimeout: null, - _touchStart: null, - _touchForwarder: new TouchForwarder(), - _shouldForwardTap: false, - _dontStopEvent: false, - _getMaximizedStatusBarWidth: function sb_getMaximizedStatusBarWidth() { // Let's consider the style of the status bar: // * padding: 0 0.3rem; @@ -777,10 +769,6 @@ var StatusBar = { }, panelHandler: function sb_panelHandler(evt) { - var app = Service.currentApp.getTopMostWindow(); - var chromeBar = app.element.querySelector('.chrome'); - var titleBar = app.element.querySelector('.titlebar'); - // Do not forward events if FTU is running if (FtuLauncher.isFtuRunning()) { return; @@ -791,142 +779,8 @@ var StatusBar = { return; } - if (this._dontStopEvent) { - return; - } - - // If the app is not a fullscreen app, let utility_tray.js handle - // this instead. - if (!document.mozFullScreen && !app.isFullScreen()) { - return; - } - - evt.stopImmediatePropagation(); - evt.preventDefault(); - - var touch; - switch (evt.type) { - case 'touchstart': - clearTimeout(this._releaseTimeout); - - var iframe = app.iframe; - this._touchForwarder.destination = iframe; - this._touchStart = evt; - this._shouldForwardTap = true; - - - touch = evt.changedTouches[0]; - this._startX = touch.clientX; - this._startY = touch.clientY; - - chromeBar.style.transition = 'transform'; - titleBar.style.transition = 'transform'; - break; - - case 'touchmove': - touch = evt.touches[0]; - var height = this._cacheHeight; - var deltaX = touch.clientX - this._startX; - var deltaY = touch.clientY - this._startY; - - if (Math.abs(deltaX) > 5 || Math.abs(deltaY) > 5) { - this._shouldForwardTap = false; - } - - var translate = Math.min(deltaY, height); - var heightThreshold = height; - - if (app && app.isFullScreen() && app.config.chrome && - app.config.chrome.navigation) { - translate = Math.min(deltaY, app.appChrome.height); - heightThreshold = app.appChrome.height; - - titleBar.style.transform = 'translateY(calc(' + - (translate - app.appChrome.height) + 'px)'; - } else { - titleBar.style.transform = - 'translateY(calc(' + translate + 'px - 100%)'; - } - chromeBar.style.transform = - 'translateY(calc(' + translate + 'px - 100%)'; - - if (translate >= heightThreshold) { - if (this._touchStart) { - this._touchForwarder.forward(this._touchStart); - this._touchStart = null; - } - this._touchForwarder.forward(evt); - } - break; - - case 'touchend': - clearTimeout(this._releaseTimeout); - - if (this._touchStart) { - if (this._shouldForwardTap) { - this._touchForwarder.forward(this._touchStart); - this._touchForwarder.forward(evt); - this._touchStart = null; - } - this._releaseBar(titleBar); - } else { - // If we already forwarded the touchstart it means the bar - // if fully open, releasing after a timeout. - this._dontStopEvent = true; - this._touchForwarder.forward(evt); - this._releaseAfterTimeout(titleBar); - } - - break; - } - }, - - _releaseBar: function sb_releaseBar(titleBar) { - this._dontStopEvent = false; - var chromeBar = titleBar.parentNode.querySelector('.chrome'); - - chromeBar.classList.remove('dragged'); - chromeBar.style.transform = ''; - chromeBar.style.transition = ''; - - titleBar.classList.remove('dragged'); - titleBar.style.transform = ''; - titleBar.style.transition = ''; - - this.screen.classList.remove('minimized-tray'); - - clearTimeout(this._releaseTimeout); - this._releaseTimeout = null; - }, - - _releaseAfterTimeout: function sb_releaseAfterTimeout(titleBar) { - this.screen.classList.add('minimized-tray'); - - var chromeBar = titleBar.parentNode.querySelector('.chrome'); - - var self = this; - titleBar.style.transform = ''; - titleBar.style.transition = ''; - titleBar.classList.add('dragged'); - - chromeBar.style.transform = ''; - chromeBar.style.transition = ''; - chromeBar.classList.add('dragged'); - - self._releaseTimeout = setTimeout(function() { - self._releaseBar(titleBar); - window.removeEventListener('touchstart', closeOnTap); - }, 5000); - - function closeOnTap(evt) { - if (evt.target != self._touchForwarder.destination) { - return; - } - - window.removeEventListener('touchstart', closeOnTap); - self._releaseBar(titleBar); - } - window.addEventListener('touchstart', closeOnTap); + var app = Service.query('getTopMostWindow'); + app && app.handleStatusbarTouch(evt, this._cacheHeight); }, /** diff --git a/apps/system/test/unit/app_statusbar_test.js b/apps/system/test/unit/app_statusbar_test.js new file mode 100644 index 000000000000..8c07c2701f26 --- /dev/null +++ b/apps/system/test/unit/app_statusbar_test.js @@ -0,0 +1,242 @@ +/* global MockAppWindow, AppStatusbar, MocksHelper, MockTouchForwarder */ +'use strict'; + +require('/shared/test/unit/mocks/mock_l10n.js'); +requireApp('system/test/unit/mock_app_window.js'); +require('/test/unit/mock_touch_forwarder.js'); +requireApp('system/js/service.js'); +requireApp('system/js/base_ui.js'); +requireApp('system/js/app_statusbar.js'); + +var mocksForAppStatusbar = new MocksHelper([ + 'AppWindow', + 'TouchForwarder' +]).init(); + +suite('system/AppStatusbar', function() { + mocksForAppStatusbar.attachTestHelpers(); + var app; + var subject; + setup(function() { + this.sinon.useFakeTimers(); + app = new MockAppWindow({ chrome: { navigation: false }}); + subject = new AppStatusbar(app); + this.sinon.stub(app, 'isFullScreen').returns(true); + subject.screen = document.createElement('div'); + subject.chromeBar = document.createElement('div'); + subject.titleBar = document.createElement('div'); + }); + + suite('fullscreen mode >', function() { + function forgeTouchEvent(type, x, y) { + var touch = document.createTouch(window, null, 42, x, y, + x, y, x, y, + 0, 0, 0, 0); + var touchList = document.createTouchList(touch); + var touches = (type == 'touchstart' || type == 'touchmove') ? + touchList : null; + var changed = (type == 'touchmove') ? + null : touchList; + + var e = document.createEvent('TouchEvent'); + e.initTouchEvent(type, true, true, + null, null, false, false, false, false, + touches, null, changed); + + return e; + } + + function forgeMouseEvent(type, x, y) { + var e = document.createEvent('MouseEvent'); + + e.initMouseEvent(type, true, true, window, 1, x, y, x, y, + false, false, false, false, 0, null); + + return e; + } + + function fakeDispatch(type, x, y) { + var e; + if (type.startsWith('mouse')) { + e = forgeMouseEvent(type, x, y); + } else { + e = forgeTouchEvent(type, x, y); + } + subject.handleStatusbarTouch(e, 24); + + return e; + } + + suite('Revealing the StatusBar >', function() { + var transitionEndSpy; + var element; + setup(function() { + element = subject.titleBar; + transitionEndSpy = this.sinon.spy(element, 'addEventListener'); + }); + + function assertStatusBarReleased() { + assert.isFalse(element.classList.contains('dragged')); + } + + teardown(function() { + this.sinon.clock.tick(10000); + }); + + test('it should prevent default on mouse events', + function() { + var mousedown = fakeDispatch('mousedown', 100, 0); + var mousemove = fakeDispatch('mousemove', 100, 2); + var mouseup = fakeDispatch('mouseup', 100, 2); + + assert.isTrue(mousedown.defaultPrevented); + assert.isTrue(mousemove.defaultPrevented); + assert.isTrue(mouseup.defaultPrevented); + }); + + test('it should prevent default on all touch events to prevent reflows', + function() { + var touchstart = fakeDispatch('touchstart', 100, 0); + var touchmove = fakeDispatch('touchmove', 100, 2); + var touchend = fakeDispatch('touchend', 100, 2); + + assert.isTrue(touchstart.defaultPrevented); + assert.isTrue(touchmove.defaultPrevented); + assert.isTrue(touchend.defaultPrevented); + }); + + test('it should stop the propagation of the events at first', function() { + var fakeEvt = { + stopImmediatePropagation: function() {}, + preventDefault: function() {}, + type: 'fake' + }; + this.sinon.spy(fakeEvt, 'stopImmediatePropagation'); + subject.handleStatusbarTouch(fakeEvt, 24); + sinon.assert.calledOnce(fakeEvt.stopImmediatePropagation); + }); + + test('it should translate the statusbar on touchmove', function() { + fakeDispatch('touchstart', 100, 0); + fakeDispatch('touchmove', 100, 5); + var transform = 'translateY(calc(5px - 100%))'; + assert.equal(element.style.transform, transform); + fakeDispatch('touchend', 100, 5); + }); + + test('it should set the dragged class on touchstart', function() { + fakeDispatch('touchstart', 100, 0); + assert.isFalse(element.classList.contains('dragged')); + fakeDispatch('touchmove', 100, 24); + fakeDispatch('touchend', 100, 25); + assert.isTrue(element.classList.contains('dragged')); + }); + + test('it should not translate the statusbar more than its height', + function() { + fakeDispatch('touchstart', 100, 0); + fakeDispatch('touchmove', 100, 5); + fakeDispatch('touchmove', 100, 15); + var transform = 'translateY(calc(15px - 100%))'; + assert.equal(element.style.transform, transform); + fakeDispatch('touchend', 100, 15); + }); + + suite('after the gesture', function() { + suite('when the StatusBar is not fully displayed', function() { + setup(function() { + fakeDispatch('touchstart', 100, 0); + fakeDispatch('touchmove', 100, 5); + fakeDispatch('touchend', 100, 5); + }); + + test('it should hide it right away', function() { + assertStatusBarReleased(); + }); + }); + + suite('when the StatusBar is fully displayed', function() { + setup(function() { + fakeDispatch('touchstart', 100, 0); + fakeDispatch('touchmove', 100, 5); + fakeDispatch('touchmove', 100, 24); + fakeDispatch('touchend', 100, 24); + }); + + test('it should not hide it right away', function() { + assert.equal(subject.titleBar.style.transform, ''); + assert.equal(subject.titleBar.style.transition, ''); + assert.ok(subject.titleBar.classList.contains('dragged')); + }); + + test('but after 5 seconds', function() { + this.sinon.clock.tick(5000); + assertStatusBarReleased(); + }); + + test('or if the user interacts with the app', function() { + // We're faking a touchstart event on the app iframe + var iframe = app.browser.element; + subject._touchForwarder.destination = window; + + var e = forgeTouchEvent('touchstart', 100, 100); + window.dispatchEvent(e); + + assertStatusBarReleased(); + subject._touchForwarder.destination = iframe; + }); + }); + }); + }); + + suite('Touch forwarding in fullscreen >', function() { + var forwardSpy; + + setup(function() { + forwardSpy = this.sinon.spy(MockTouchForwarder.prototype, 'forward'); + }); + + test('it should forward taps to the app', function() { + var touchstart = fakeDispatch('touchstart', 100, 0); + fakeDispatch('touchmove', 100, 2); + var touchend = fakeDispatch('touchend', 100, 2); + + assert.isTrue(forwardSpy.calledTwice); + var call = forwardSpy.firstCall; + assert.equal(call.args[0], touchstart); + call = forwardSpy.getCall(1); + assert.equal(call.args[0], touchend); + }); + + suite('if it\'s not a tap and the statusbar is not fully displayed', + function() { + test('it should not forward any events', function() { + fakeDispatch('touchstart', 100, 0); + fakeDispatch('touchmove', 100, 8); + fakeDispatch('touchend', 100, 8); + + assert.isTrue(forwardSpy.notCalled); + }); + }); + + test('it should forward touchmove once the statusbar is shown', + function() { + var touchstart = fakeDispatch('touchstart', 100, 0); + fakeDispatch('touchmove', 100, 6); + var secondMove = fakeDispatch('touchmove', 100, 26); + var thirdMove = fakeDispatch('touchmove', 100, 28); + var touchend = fakeDispatch('touchend', 100, 2); + + assert.equal(forwardSpy.callCount, 4); + var call = forwardSpy.firstCall; + assert.equal(call.args[0], touchstart); + call = forwardSpy.getCall(1); + assert.equal(call.args[0], secondMove); + call = forwardSpy.getCall(2); + assert.equal(call.args[0], thirdMove); + call = forwardSpy.getCall(3); + assert.equal(call.args[0], touchend); + }); + }); + }); +}); diff --git a/apps/system/test/unit/app_window_test.js b/apps/system/test/unit/app_window_test.js index e358470892e3..4be90add66da 100644 --- a/apps/system/test/unit/app_window_test.js +++ b/apps/system/test/unit/app_window_test.js @@ -2477,4 +2477,15 @@ suite('system/AppWindow', function() { appInput.element.dispatchEvent(new CustomEvent('_opened')); assert.equal(appInput.appChrome, undefined); }); + + test('Should bypass touch event to statusbar submodule', function() { + var app = new AppWindow(fakeAppConfig1); + app.statusbar = { + handleStatusbarTouch: this.sinon.spy() + }; + var fakeTouchEvt = new CustomEvent('touchstart'); + app.handleStatusbarTouch(fakeTouchEvt, 24); + assert.isTrue(app.statusbar.handleStatusbarTouch.calledWith( + fakeTouchEvt, 24)); + }); }); diff --git a/apps/system/test/unit/mock_app_window.js b/apps/system/test/unit/mock_app_window.js index 24711af8fe98..45539b30714f 100644 --- a/apps/system/test/unit/mock_app_window.js +++ b/apps/system/test/unit/mock_app_window.js @@ -10,7 +10,11 @@ var MockAppWindow = function AppWindow(config) { if (config) { for (var key in config) { - this[key] = config[key]; + try { + this[key] = config[key]; + } catch (e) { + + } } this.config = config; } @@ -37,6 +41,12 @@ } return this._element; }, + get titleBar() { + if (!this._titleBar) { + this._titleBar = document.createElement('div'); + } + return this._titleBar; + }, get browser() { if (!this._iframe) { this._iframe = document.createElement('iframe'); @@ -46,6 +56,16 @@ element: this._iframe }; }, + get frame() { + return this.element; + }, + get iframe() { + if (!this._iframe) { + this._iframe = document.createElement('iframe'); + this._iframe.download = function() {}; + } + return this._iframe; + }, render: function() {}, open: function() {}, close: function() {}, @@ -120,7 +140,8 @@ '_resize': function() {}, isForeground: function() {}, killable: function() {}, - setVisibileForScreenReader: function() {} + setVisibileForScreenReader: function() {}, + handleStatusbarTouch: function() {} }; MockAppWindow.mTeardown = function() { MockAppWindowHelper.mInstances = []; diff --git a/apps/system/test/unit/statusbar_test.js b/apps/system/test/unit/statusbar_test.js index c54dbc54c04d..fa7c7184ca33 100644 --- a/apps/system/test/unit/statusbar_test.js +++ b/apps/system/test/unit/statusbar_test.js @@ -1,9 +1,9 @@ /* globals FtuLauncher, MockL10n, MockMobileOperator, MockLayoutManager, MockNavigatorMozMobileConnections, MockNavigatorMozTelephony, MockSettingsListener, MocksHelper, MockSIMSlot, MockSIMSlotManager, - MockService, MockTouchForwarder, StatusBar, Service, + MockService, StatusBar, Service, MockNfcManager, MockMobileconnection, MockAppWindowManager, - MockNavigatorBattery, UtilityTray */ + MockNavigatorBattery, UtilityTray, MockAppWindow */ 'use strict'; require('/shared/test/unit/mocks/mock_settings_listener.js'); @@ -24,17 +24,18 @@ require('/test/unit/mock_touch_forwarder.js'); require('/test/unit/mock_utility_tray.js'); require('/test/unit/mock_layout_manager.js'); require('/test/unit/mock_navigator_battery.js'); +require('/test/unit/mock_app_window.js'); var mocksForStatusBar = new MocksHelper([ - 'FtuLauncher', 'SettingsListener', 'MobileOperator', 'SIMSlotManager', - 'AppWindowManager', - 'TouchForwarder', 'UtilityTray', 'LayoutManager', - 'NavigatorBattery' + 'NavigatorBattery', + 'AppWindow', + 'Service', + 'FtuLauncher' ]).init(); suite('system/Statusbar', function() { @@ -1505,21 +1506,21 @@ suite('system/Statusbar', function() { suite('fullscreen mode >', function() { function forgeTouchEvent(type, x, y) { - var touch = document.createTouch(window, null, 42, x, y, - x, y, x, y, - 0, 0, 0, 0); - var touchList = document.createTouchList(touch); - var touches = (type == 'touchstart' || type == 'touchmove') ? - touchList : null; - var changed = (type == 'touchmove') ? - null : touchList; - - var e = document.createEvent('TouchEvent'); - e.initTouchEvent(type, true, true, - null, null, false, false, false, false, - touches, null, changed); - - return e; + var touch = document.createTouch(window, null, 42, x, y, + x, y, x, y, + 0, 0, 0, 0); + var touchList = document.createTouchList(touch); + var touches = (type == 'touchstart' || type == 'touchmove') ? + touchList : null; + var changed = (type == 'touchmove') ? + null : touchList; + + var e = document.createEvent('TouchEvent'); + e.initTouchEvent(type, true, true, + null, null, false, false, false, false, + touches, null, changed); + + return e; } function forgeMouseEvent(type, x, y) { @@ -1545,37 +1546,9 @@ suite('system/Statusbar', function() { var app; setup(function() { - app = { - isFullScreen: function() { - return true; - }, - titleBar: { - element: document.createElement('div') - }, - iframe: document.createElement('iframe'), - getTopMostWindow: function() { - return app; - }, - config: {}, - _element: null, - get element() { - if (!this._element) { - var element = document.createElement('div'); - var title = document.createElement('div'); - title.classList.add('titlebar'); - element.appendChild(title); - - var chrome = document.createElement('div'); - chrome.className = 'chrome'; - element.appendChild(chrome); - this._element = element; - } - - return this._element; - } - }; - - Service.currentApp = app; + app = new MockAppWindow(); + MockService.mTopMostWindow = app; + this.sinon.stub(app, 'handleStatusbarTouch'); this.sinon.stub(StatusBar.element, 'getBoundingClientRect').returns({ height: 10 }); @@ -1584,62 +1557,34 @@ suite('system/Statusbar', function() { }); suite('Revealing the StatusBar >', function() { - var transitionEndSpy; - var element; setup(function() { StatusBar._cacheHeight = 24; - element = Service.currentApp.element - .querySelector('.titlebar'); - transitionEndSpy = this.sinon.spy(element, 'addEventListener'); }); - function assertStatusBarReleased() { - assert.equal(StatusBar.element.style.transform, ''); - assert.equal(StatusBar.element.style.transition, ''); - - assert.isFalse(element.classList.contains('dragged')); - } - teardown(function() { this.sinon.clock.tick(10000); StatusBar.element.style.transition = ''; StatusBar.element.style.transform = ''; }); - test('it should stop the propagation of the events at first', function() { - var fakeEvt = { - stopImmediatePropagation: function() {}, - preventDefault: function() {}, - type: 'fake' - }; - this.sinon.spy(fakeEvt, 'stopImmediatePropagation'); - StatusBar.panelHandler(fakeEvt); - sinon.assert.calledOnce(fakeEvt.stopImmediatePropagation); - }); - test('it should translate the statusbar on touchmove', function() { fakeDispatch('touchstart', 100, 0); - fakeDispatch('touchmove', 100, 5); - var transform = 'translateY(calc(5px - 100%))'; - assert.equal(element.style.transform, transform); + var evt = fakeDispatch('touchmove', 100, 5); + assert.isTrue(app.handleStatusbarTouch.calledWith(evt, 24)); fakeDispatch('touchend', 100, 5); }); - test('it should set the dragged class on touchstart', function() { - fakeDispatch('touchstart', 100, 0); - assert.isFalse(element.classList.contains('dragged')); - fakeDispatch('touchmove', 100, 24); - fakeDispatch('touchend', 100, 25); - assert.isTrue(element.classList.contains('dragged')); + test('it should bypass touchstart event', function() { + var evt = fakeDispatch('touchstart', 100, 0); + assert.isTrue(app.handleStatusbarTouch.calledWith(evt, 24)); }); test('it should not translate the statusbar more than its height', function() { fakeDispatch('touchstart', 100, 0); fakeDispatch('touchmove', 100, 5); - fakeDispatch('touchmove', 100, 15); - var transform = 'translateY(calc(15px - 100%))'; - assert.equal(element.style.transform, transform); + var evt = fakeDispatch('touchmove', 100, 15); + assert.isTrue(app.handleStatusbarTouch.calledWith(evt, 24)); fakeDispatch('touchend', 100, 15); }); @@ -1664,10 +1609,8 @@ suite('system/Statusbar', function() { FtuLauncher.mIsRunning = true; fakeDispatch('touchstart', 100, 0); fakeDispatch('touchmove', 100, 100); - - var titleEl = Service.currentApp.element - .querySelector('.titlebar'); - assert.equal(titleEl.style.transform, ''); + + assert.isFalse(app.handleStatusbarTouch.called); FtuLauncher.mIsRunning = false; }); @@ -1676,140 +1619,9 @@ suite('system/Statusbar', function() { fakeDispatch('touchstart', 100, 0); fakeDispatch('touchmove', 100, 100); - var titleEl = Service.currentApp.element - .querySelector('.titlebar'); - assert.equal(titleEl.style.transform, ''); + assert.isFalse(app.handleStatusbarTouch.called); UtilityTray.active = false; }); - - - suite('after the gesture', function() { - suite('when the StatusBar is not fully displayed', function() { - setup(function() { - fakeDispatch('touchstart', 100, 0); - fakeDispatch('touchmove', 100, 5); - fakeDispatch('touchend', 100, 5); - }); - - test('it should hide it right away', function() { - assertStatusBarReleased(); - }); - }); - - suite('when the StatusBar is fully displayed', function() { - setup(function() { - fakeDispatch('touchstart', 100, 0); - fakeDispatch('touchmove', 100, 5); - fakeDispatch('touchmove', 100, 24); - fakeDispatch('touchend', 100, 24); - }); - - test('it should not hide it right away', function() { - var titleEl = Service.currentApp.element - .querySelector('.titlebar'); - assert.equal(titleEl.style.transform, ''); - assert.equal(titleEl.style.transition, ''); - assert.ok(titleEl.classList.contains('dragged')); - }); - - test('but after 5 seconds', function() { - this.sinon.clock.tick(5000); - assertStatusBarReleased(); - }); - - test('or if the user interacts with the app', function() { - // We're faking a touchstart event on the app iframe - var iframe = StatusBar._touchForwarder.destination; - StatusBar._touchForwarder.destination = window; - - var e = forgeTouchEvent('touchstart', 100, 100); - window.dispatchEvent(e); - - assertStatusBarReleased(); - StatusBar._touchForwarder.destination = iframe; - }); - }); - }); - }); - - test('it should prevent default on mouse events keep the focus on the app', - function() { - var mousedown = fakeDispatch('mousedown', 100, 0); - var mousemove = fakeDispatch('mousemove', 100, 2); - var mouseup = fakeDispatch('mouseup', 100, 2); - - assert.isTrue(mousedown.defaultPrevented); - assert.isTrue(mousemove.defaultPrevented); - assert.isTrue(mouseup.defaultPrevented); - }); - - suite('Touch forwarding in fullscreen >', function() { - var forwardSpy; - - setup(function() { - StatusBar._cacheHeight = 24; - forwardSpy = this.sinon.spy(MockTouchForwarder.prototype, 'forward'); - }); - - test('it should prevent default on all touch events to prevent reflows', - function() { - var touchstart = fakeDispatch('touchstart', 100, 0); - var touchmove = fakeDispatch('touchmove', 100, 2); - var touchend = fakeDispatch('touchend', 100, 2); - - assert.isTrue(touchstart.defaultPrevented); - assert.isTrue(touchmove.defaultPrevented); - assert.isTrue(touchend.defaultPrevented); - }); - - test('it should set the destination of the TouchForwarder on touchstart', - function() { - fakeDispatch('touchstart', 100, 0); - assert.equal(StatusBar._touchForwarder.destination, app.iframe); - fakeDispatch('touchend', 100, 0); - }); - - test('it should forward taps to the app', function() { - var touchstart = fakeDispatch('touchstart', 100, 0); - fakeDispatch('touchmove', 100, 2); - var touchend = fakeDispatch('touchend', 100, 2); - - assert.isTrue(forwardSpy.calledTwice); - var call = forwardSpy.firstCall; - assert.equal(call.args[0], touchstart); - call = forwardSpy.getCall(1); - assert.equal(call.args[0], touchend); - }); - - suite('if it\'s not a tap and the statusbar is not fully displayed', - function() { - test('it should not forward any events', function() { - fakeDispatch('touchstart', 100, 0); - fakeDispatch('touchmove', 100, 8); - fakeDispatch('touchend', 100, 8); - - assert.isTrue(forwardSpy.notCalled); - }); - }); - - test('it should forward touchmove once the statusbar is shown', - function() { - var touchstart = fakeDispatch('touchstart', 100, 0); - fakeDispatch('touchmove', 100, 6); - var secondMove = fakeDispatch('touchmove', 100, 26); - var thirdMove = fakeDispatch('touchmove', 100, 28); - var touchend = fakeDispatch('touchend', 100, 2); - - assert.equal(forwardSpy.callCount, 4); - var call = forwardSpy.firstCall; - assert.equal(call.args[0], touchstart); - call = forwardSpy.getCall(1); - assert.equal(call.args[0], secondMove); - call = forwardSpy.getCall(2); - assert.equal(call.args[0], thirdMove); - call = forwardSpy.getCall(3); - assert.equal(call.args[0], touchend); - }); }); }); diff --git a/apps/system/test/unit/task_manager_test.js b/apps/system/test/unit/task_manager_test.js index c66532e91c80..deee84459160 100644 --- a/apps/system/test/unit/task_manager_test.js +++ b/apps/system/test/unit/task_manager_test.js @@ -115,9 +115,7 @@ suite('system/TaskManager >', function() { 'http://sms.gaiamobile.org': new AppWindow({ launchTime: 5, name: 'SMS', - element: document.createElement('div'), frame: document.createElement('div'), - iframe: document.createElement('iframe'), manifest: { orientation: 'portrait-primary' }, @@ -137,9 +135,6 @@ suite('system/TaskManager >', function() { 'http://game.gaiamobile.org': new AppWindow({ launchTime: 4, name: 'GAME', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), manifest: { orientation: 'landscape-primary' }, @@ -156,9 +151,6 @@ suite('system/TaskManager >', function() { 'browser1': new AppWindow({ launchTime: 4, name: 'BROWSER1', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), rotatingDegree: 0, isBrowser: function() { return true; @@ -175,9 +167,6 @@ suite('system/TaskManager >', function() { 'http://game2.gaiamobile.org': new AppWindow({ launchTime: 3, name: 'GAME2', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), manifest: { orientation: 'landscape-secondary' }, @@ -193,9 +182,6 @@ suite('system/TaskManager >', function() { }), 'browser2': new AppWindow({ name: 'BROWSER2', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), rotatingDegree: 0, isBrowser: function() { return true; @@ -211,9 +197,6 @@ suite('system/TaskManager >', function() { }), 'search': new AppWindow({ name: 'search', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), rotatingDegree: 0, isBrowser: function() { return true; @@ -236,9 +219,6 @@ suite('system/TaskManager >', function() { instanceID: 'AppWindow-0', launchTime: 5, name: 'SMS', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), manifest: { orientation: 'portrait-primary' }, @@ -257,9 +237,6 @@ suite('system/TaskManager >', function() { instanceID: 'AppWindow-1', launchTime: 5, name: 'GAME', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), manifest: { orientation: 'portrait-primary' }, @@ -278,9 +255,6 @@ suite('system/TaskManager >', function() { instanceID: 'AppWindow-2', launchTime: 5, name: 'GAME2', - element: document.createElement('div'), - frame: document.createElement('div'), - iframe: document.createElement('iframe'), manifest: { orientation: 'portrait-primary' }, diff --git a/shared/test/unit/mocks/mock_service.js b/shared/test/unit/mocks/mock_service.js index b43efae12e3e..c993d5024671 100644 --- a/shared/test/unit/mocks/mock_service.js +++ b/shared/test/unit/mocks/mock_service.js @@ -20,6 +20,8 @@ var MockService = { return this.runningFTU; case 'isFtuUpgrading': return this.mUpgrading; + case 'getTopMostWindow': + return this.mTopMostWindow; } return undefined; },