diff --git a/app/browser/reducers/windowsReducer.js b/app/browser/reducers/windowsReducer.js index d93fbc4fa45..1facd173926 100644 --- a/app/browser/reducers/windowsReducer.js +++ b/app/browser/reducers/windowsReducer.js @@ -23,14 +23,10 @@ const electron = require('electron') const BrowserWindow = electron.BrowserWindow const firstDefinedValue = require('../../../js/lib/functional').firstDefinedValue const appConfig = require('../../../js/constants/appConfig') -const messages = require('../../../js/constants/messages') -const appUrlUtil = require('../../../js/lib/appUrlUtil') const settings = require('../../../js/constants/settings') const getSetting = require('../../../js/settings').getSetting -const {zoomLevel} = require('../../common/constants/toolbarUserInterfaceScale') + const platformUtil = require('../../common/lib/platformUtil') -const {initWindowCacheState} = require('../../sessionStoreShutdown') -const appDispatcher = require('../../../js/dispatcher/appDispatcher') const isDarwin = platformUtil.isDarwin() const isWindows = platformUtil.isWindows() @@ -43,17 +39,43 @@ function isModal (browserOpts) { const navbarHeight = () => { // TODO there has to be a better way to get this or at least add a test + // TODO try creating a window and measuring the difference between window and content area + // and updating this number with that value once the first window is created return 75 } +function clearFramesFromWindowState (windowState) { + return windowState + .set('frames', Immutable.List()) + .set('tabs', Immutable.List()) +} + +/** + * Determine the frame(s) to be loaded in a new window + * based on user preferences + */ +function getFramesForNewWindow () { + const startupSetting = getSetting(settings.STARTUP_MODE) + const homepageSetting = getSetting(settings.HOMEPAGE) + if (startupSetting === 'homePage' && homepageSetting) { + return homepageSetting + .split('|') + .map((homepage) => ({ + location: homepage + })) + } + return [ { } ] +} + /** * Determine window dimensions (width / height) */ const setWindowDimensions = (browserOpts, defaults, immutableWindowState) => { assert(isImmutable(immutableWindowState)) - if (immutableWindowState.getIn(['windowInfo'])) { - browserOpts.width = firstDefinedValue(browserOpts.width, immutableWindowState.getIn(['windowInfo', 'width'])) - browserOpts.height = firstDefinedValue(browserOpts.height, immutableWindowState.getIn(['windowInfo', 'height'])) + const windowInfoState = immutableWindowState.get('windowInfo') + if (windowInfoState) { + browserOpts.width = firstDefinedValue(browserOpts.width, windowInfoState.get('width')) + browserOpts.height = firstDefinedValue(browserOpts.height, windowInfoState.get('windowInfo')) } else { browserOpts.width = firstDefinedValue(browserOpts.width, browserOpts.innerWidth, defaults.width) // height and innerHeight are the frame webview size @@ -108,7 +130,6 @@ const setMaximized = (state, browserOpts, immutableWindowState) => { function windowDefaults (state) { return { - show: false, width: state.getIn(['defaultWindowParams', 'width']) || state.get('defaultWindowWidth'), height: state.getIn(['defaultWindowParams', 'height']) || state.get('defaultWindowHeight'), x: state.getIn(['defaultWindowParams', 'x']) || undefined, @@ -146,10 +167,10 @@ function setDefaultWindowSize (state) { return state } -const createWindow = (state, action) => { +const handleCreateWindowAction = (state, action) => { const frameOpts = (action.get('frameOpts') || Immutable.Map()).toJS() let browserOpts = (action.get('browserOpts') || Immutable.Map()).toJS() - const immutableWindowState = action.get('restoredState') || Immutable.Map() + let immutableWindowState = action.get('restoredState') || Immutable.Map() state = setDefaultWindowSize(state) const defaults = windowDefaults(state) const isMaximized = setMaximized(state, browserOpts, immutableWindowState) @@ -160,21 +181,30 @@ const createWindow = (state, action) => { delete browserOpts.left delete browserOpts.top + // decide which bounds to restrict new window to const screen = electron.screen + // use primary display by default let primaryDisplay = screen.getPrimaryDisplay() - const parentWindowKey = browserOpts.parentWindowKey - if (browserOpts.x != null && browserOpts.y != null) { - const matchingDisplay = screen.getDisplayMatching(browserOpts) + // can override with provided x, y coords + if (browserOpts.x != null && browserOpts.y != null && browserOpts.width != null && browserOpts.height != null) { + const matchingDisplay = screen.getDisplayMatching({ + x: browserOpts.x, + y: browserOpts.y, + width: browserOpts.width, + height: browserOpts.height + }) if (matchingDisplay != null) { primaryDisplay = matchingDisplay } } - + // always override with parent window if present + const parentWindowKey = browserOpts.parentWindowKey const parentWindow = parentWindowKey ? BrowserWindow.fromId(parentWindowKey) : BrowserWindow.getFocusedWindow() const bounds = parentWindow ? parentWindow.getBounds() : primaryDisplay.bounds + // decide which screen to position on // position on screen should be relative to focused window // or the primary display if there is no focused window const display = screen.getDisplayNearestPoint(bounds) @@ -229,7 +259,8 @@ const createWindow = (state, action) => { autoHideMenuBar: autoHideMenuBarSetting, title: appConfig.name, webPreferences: defaults.webPreferences, - frame: !isWindows + frame: !isWindows, + disposition: frameOpts.disposition } if (process.platform === 'linux') { @@ -239,82 +270,35 @@ const createWindow = (state, action) => { if (immutableWindowState.getIn(['windowInfo', 'state']) === 'fullscreen') { windowProps.fullscreen = true } - - const homepageSetting = getSetting(settings.HOMEPAGE) - const startupSetting = getSetting(settings.STARTUP_MODE) - const toolbarUserInterfaceScale = getSetting(settings.TOOLBAR_UI_SCALE) - + // continue with window creation process outside of store action handler setImmediate(() => { - const win = new BrowserWindow(Object.assign(windowProps, browserOpts, {disposition: frameOpts.disposition})) - let restoredImmutableWindowState = action.get('restoredState') - initWindowCacheState(win.id, restoredImmutableWindowState) - - // initialize frames state - let frames = Immutable.List() - if (restoredImmutableWindowState && restoredImmutableWindowState.get('frames', Immutable.List()).size > 0) { - frames = restoredImmutableWindowState.get('frames') - restoredImmutableWindowState = restoredImmutableWindowState.set('frames', Immutable.List()) - restoredImmutableWindowState = restoredImmutableWindowState.set('tabs', Immutable.List()) + // decide which frames to load in the window + let frames + // handle frames from restored state + const immutableFrames = immutableWindowState.get('frames') + if (Immutable.List.isList(immutableFrames) && immutableFrames.count()) { + frames = immutableFrames.toJS() } else { - if (frameOpts && Object.keys(frameOpts).length > 0) { - if (frameOpts.forEach) { - frames = Immutable.fromJS(frameOpts) + // handle frames from action + // can be single object or multiple in array + if (frameOpts && Object.keys(frameOpts).length) { + if (Array.isArray(frameOpts)) { + frames = frameOpts } else { - frames = frames.push(Immutable.fromJS(frameOpts)) + frames = [ frameOpts ] } - } else if (startupSetting === 'homePage' && homepageSetting) { - frames = Immutable.fromJS(homepageSetting.split('|').map((homepage) => { - return { - location: homepage - } - })) + } else { + // handle nothing provided, so follow 'new tab' preferences + frames = getFramesForNewWindow() } } - - if (frames.size === 0) { - frames = Immutable.fromJS([{}]) - } - - if (isMaximized) { - win.maximize() - } - - appDispatcher.registerWindow(win, win.webContents) - win.webContents.on('did-finish-load', (e) => { - const appStore = require('../../../js/stores/appStore') - win.webContents.setZoomLevel(zoomLevel[toolbarUserInterfaceScale] || 0.0) - - const position = win.getPosition() - const size = win.getSize() - - const mem = muon.shared_memory.create({ - windowValue: { - disposition: frameOpts.disposition, - id: win.id, - focused: win.isFocused(), - left: position[0], - top: position[1], - height: size[1], - width: size[0] - }, - appState: appStore.getLastEmittedState().toJS(), - frames: frames.toJS(), - windowState: (restoredImmutableWindowState && restoredImmutableWindowState.toJS()) || undefined - }) - - e.sender.sendShared(messages.INITIALIZE_WINDOW, mem) - if (action.cb) { - action.cb() - } - }) - - win.on('ready-to-show', () => { - win.show() - }) - - win.loadURL(appUrlUtil.getBraveExtIndexHTML()) + // window does not need to receive frames as part of initial state + immutableWindowState = clearFramesFromWindowState(immutableWindowState) + // allow override of defaults with incoming action argument + const windowOptions = Object.assign(windowProps, browserOpts) + // instruct muon to create window + windows.createWindow(windowOptions, parentWindow, isMaximized, frames, immutableWindowState, true, action.cb) }) - return state } @@ -325,11 +309,14 @@ const windowsReducer = (state, action, immutableAction) => { state = windows.init(state, action) break case appConstants.APP_NEW_WINDOW: - state = createWindow(state, action) + state = handleCreateWindowAction(state, action) break case appConstants.APP_WINDOW_READY: windows.windowReady(action.get('windowId')) break + case appConstants.APP_WINDOW_RENDERED: + windows.windowRendered(action.get('windowId')) + break case appConstants.APP_TAB_UPDATED: if (immutableAction.getIn(['changeInfo', 'pinned']) != null) { setImmediate(() => { diff --git a/app/browser/windows.js b/app/browser/windows.js index 5971c659c4b..0d0d731f11c 100644 --- a/app/browser/windows.js +++ b/app/browser/windows.js @@ -2,7 +2,8 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this file, * You can obtain one at http://mozilla.org/MPL/2.0/. */ -const {app, BrowserWindow, ipcMain} = require('electron') +const electron = require('electron') +const Immutable = require('immutable') const appActions = require('../../js/actions/appActions') const appUrlUtil = require('../../js/lib/appUrlUtil') const {getLocationIfPDF} = require('../../js/lib/urlutil') @@ -10,12 +11,20 @@ const debounce = require('../../js/lib/debounce') const {getSetting} = require('../../js/settings') const locale = require('../locale') const LocalShortcuts = require('../localShortcuts') +const {initWindowCacheState} = require('../sessionStoreShutdown') const {makeImmutable} = require('../common/state/immutableUtil') const {getPinnedTabsByWindowId} = require('../common/state/tabState') const messages = require('../../js/constants/messages') const settings = require('../../js/constants/settings') +const config = require('../../js/constants/config') +const appDispatcher = require('../../js/dispatcher/appDispatcher') +const platformUtil = require('../common/lib/platformUtil') const windowState = require('../common/state/windowState') const pinnedSitesState = require('../common/state/pinnedSitesState') +const {zoomLevel} = require('../common/constants/toolbarUserInterfaceScale') + +const isDarwin = platformUtil.isDarwin() +const {app, BrowserWindow, ipcMain} = electron // TODO(bridiver) - set window uuid let currentWindows = {} @@ -111,6 +120,26 @@ const updatePinnedTabs = (win) => { } } +function showDeferredShowWindow (win) { + win.show() + if (win.__shouldFullscreen) { + // this timeout helps with an issue that + // when a user is loading from state, and + // has many full screen windows and non fullscreen windows + // the non fullscreen windows can get opened on top of the fullscreen + // spaces because macOS has switched away from the desktop space + setTimeout(() => { + win.setFullScreen(true) + }, 100) + } else if (win.__shouldMaximize) { + win.maximize() + } + // reset temporary properties on win object + win.__showWhenRendered = undefined + win.__shouldFullscreen = undefined + win.__shouldMaximize = undefined +} + const api = { init: (state, action) => { app.on('browser-window-created', function (event, win) { @@ -317,6 +346,19 @@ const api = { }) }, + windowRendered: (windowIdOrWin) => { + setImmediate(() => { + const win = windowIdOrWin instanceof electron.BrowserWindow + ? windowIdOrWin + : currentWindows[windowIdOrWin] + if (win && win.__showWhenRendered && !win.isDestroyed() && !win.isVisible()) { + // window is hidden by default until we receive 'ready' message, + // so show it now + showDeferredShowWindow(win) + } + }) + }, + closeWindow: (windowId) => { let win = api.getWindow(windowId) try { @@ -330,6 +372,108 @@ const api = { } }, + createWindow: function (windowOptionsIn, parentWindow, maximized, frames, immutableState = Immutable.Map(), hideUntilRendered = true, cb = null) { + const defaultOptions = { + // hide the window until the window reports that it is rendered + show: true, + fullscreenable: true + } + const windowOptions = Object.assign( + defaultOptions, + windowOptionsIn + ) + // will only hide until rendered if the options specify to show window + // so that a caller can control showing the window themselves with the option { show: false } + const showWhenRendered = hideUntilRendered && windowOptions.show + if (showWhenRendered) { + // prevent browserwindow from opening window immediately + windowOptions.show = false + } + // normally macOS will open immediately-created windows from fullscreen + // parent windows as fullscreen + // but if we are showing the window async, we will set the window + // fullscreen once it is ready to be shown + // (windowOptionsIn.fullscreen may already be set when loading from saved state, + // so this just sets it for other scenarios) + if (showWhenRendered && isDarwin && parentWindow && parentWindow.isFullScreen()) { + windowOptions.fullscreen = true + } + // if delaying window show, remember if the window should be opened fullscreen + // and remove the fullscreen property for now + // (otherwise the window will be shown immediately by macOS / muon) + let fullscreenWhenRendered = false + if (showWhenRendered && windowOptions.fullscreen) { + windowOptions.fullscreen = false + fullscreenWhenRendered = true + } + // create window with Url to renderer + const win = new electron.BrowserWindow(windowOptions) + win.loadURL(appUrlUtil.getBraveExtIndexHTML()) + // TODO: pass UUID + initWindowCacheState(win.id, immutableState) + // let the windowReady handler know to show the window + win.__showWhenRendered = showWhenRendered + if (win.__showWhenRendered) { + // let the windowReady handler know to set the window state + win.__shouldFullscreen = fullscreenWhenRendered + win.__shouldMaximize = maximized + // the window is hidden until render, but we'll check to see + // if it is shown in a timeout as, if the window errors, it won't send + // the message to ask to be shown + // in those cases, we want to still show it, so that the user can find the error message + setTimeout(() => { + if (win && !win.isDestroyed() && !win.isVisible()) { + showDeferredShowWindow(win) + } + }, config.windows.timeoutToShowWindowMs) + } else { + // window should be shown already + // manual maximize + if (maximized) { + win.maximize() + } + // NOTE: we don't need to fullscreen manually since it's specified in options + // passed to BrowserWindow constructor + } + // let store know there's a new window + // so it can subscrive to state updates + appDispatcher.registerWindow(win, win.webContents) + // when window has finished loading, assume it has communications + // handler setup, and then send state + win.webContents.on('did-finish-load', (e) => { + const appStore = require('../../js/stores/appStore') + const toolbarUserInterfaceScale = getSetting(settings.TOOLBAR_UI_SCALE) + win.webContents.setZoomLevel(zoomLevel[toolbarUserInterfaceScale] || 0.0) + + const position = win.getPosition() + const size = win.getSize() + const windowState = (immutableState && immutableState.toJS()) || undefined + const mem = muon.shared_memory.create({ + windowValue: { + disposition: windowOptions.disposition, + id: win.id, + focused: win.isFocused(), + left: position[0], + top: position[1], + height: size[1], + width: size[0] + }, + appState: appStore.getLastEmittedState().toJS(), + windowState, + // TODO: dispatch frame create action on appStore, as this is what the window does anyway + // ...and do it after the window has rendered + frames + }) + + e.sender.sendShared(messages.INITIALIZE_WINDOW, mem) + // TODO: remove callback, use store action, returning a new window UUID from this function + if (cb) { + cb() + } + }) + return win + }, + getWindow: (windowId) => { return currentWindows[windowId] }, diff --git a/js/actions/appActions.js b/js/actions/appActions.js index b81a533cc6c..f30e6b54244 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -48,6 +48,16 @@ const appActions = { }) }, + windowRendered: function (windowId) { + dispatch({ + actionType: appConstants.APP_WINDOW_RENDERED, + windowId, + queryInfo: { + windowId + } + }) + }, + closeWindow: function (windowId) { dispatch({ actionType: appConstants.APP_CLOSE_WINDOW, diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index 4a4a2e4a189..6fec46fee43 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -7,6 +7,7 @@ const _ = null const appConstants = { APP_NEW_WINDOW: _, APP_WINDOW_READY: _, + APP_WINDOW_RENDERED: _, APP_CLOSE_WINDOW: _, APP_WINDOW_CLOSED: _, APP_WINDOW_CREATED: _, diff --git a/js/constants/config.js b/js/constants/config.js index 11cfac53e5e..41a7ff5e4d0 100644 --- a/js/constants/config.js +++ b/js/constants/config.js @@ -97,5 +97,8 @@ module.exports = { tabs: { maxAllowedNewSessions: 9 }, + windows: { + timeoutToShowWindowMs: 5000 + }, iconSize: 16 } diff --git a/js/entry.js b/js/entry.js index e0e8c4dac61..a31926b4088 100644 --- a/js/entry.js +++ b/js/entry.js @@ -85,9 +85,13 @@ ipc.on(messages.INITIALIZE_WINDOW, (e, mem) => { windowStore.state = newState generateTabs(newState, message.frames, windowValue.id) appActions.windowReady(windowValue.id, windowValue) - ReactDOM.render(, document.getElementById('appContainer')) + ReactDOM.render(, document.getElementById('appContainer'), fireOnReactRender.bind(null, windowValue)) }) +const fireOnReactRender = (windowValue) => { + appActions.windowRendered(windowValue.id) +} + const generateTabs = (windowState, frames, windowId) => { const activeFrameKey = windowState.get('activeFrameKey') diff --git a/test/unit/app/browser/reducers/downloadsReducerTest.js b/test/unit/app/browser/reducers/downloadsReducerTest.js index d2bd0a0a1a5..0baffce3ad9 100644 --- a/test/unit/app/browser/reducers/downloadsReducerTest.js +++ b/test/unit/app/browser/reducers/downloadsReducerTest.js @@ -148,20 +148,26 @@ describe('downloadsReducer', function () { }) describe('APP_DOWNLOAD_REDOWNLOADED', function () { - it('should redownload the same URL', function (cb) { - const win = { - webContents: { - downloadURL: function () { - } + const win = { + webContents: { + downloadURL: function () { } } + } + let spy + before(() => { + spy = sinon.stub(fakeElectron.BrowserWindow, 'getFocusedWindow', (path) => { + return win + }) + }) + after(() => { + spy.restore() + }) + it('should redownload the same URL', function (cb) { sinon.stub(win.webContents, 'downloadURL', (redownloadUrl) => { assert.equal(redownloadUrl, downloadUrl) cb() }) - sinon.stub(fakeElectron.BrowserWindow, 'getFocusedWindow', (path) => { - return win - }) const oldState = oneDownloadWithState(CANCELLED) downloadsReducer(oldState, {actionType: appConstants.APP_DOWNLOAD_REDOWNLOADED, downloadId: downloadId(oldState)}) }) diff --git a/test/unit/app/browser/reducers/windowsReducerTest.js b/test/unit/app/browser/reducers/windowsReducerTest.js index 1809eb50925..a343056f272 100644 --- a/test/unit/app/browser/reducers/windowsReducerTest.js +++ b/test/unit/app/browser/reducers/windowsReducerTest.js @@ -6,8 +6,9 @@ const mockery = require('mockery') const sinon = require('sinon') const Immutable = require('immutable') -const assert = require('assert') +const { assert } = require('chai') const fakeAdBlock = require('../../../lib/fakeAdBlock') +const FakeElectronDisplay = require('../../../lib/fakeElectronDisplay') const appConstants = require('../../../../../js/constants/appConstants') require('../../../braveUnit') @@ -20,36 +21,192 @@ describe('windowsReducer unit test', function () { maybeCreateWindow: (state, action) => state } + const fakeWindowApi = { + createWindow: () => {} + } + + const fakePlatformUtil = { + isDarwin: () => true, + isWindows: () => false + } + const state = Immutable.fromJS({ windows: [], defaultWindowParams: {} }) - + let fakeTimers before(function () { mockery.enable({ warnOnReplace: false, warnOnUnregistered: false, useCleanCache: true }) + fakeTimers = sinon.useFakeTimers() mockery.registerMock('electron', fakeElectron) mockery.registerMock('ad-block', fakeAdBlock) mockery.registerMock('../../common/state/windowState', fakeWindowState) + mockery.registerMock('../windows', fakeWindowApi) + mockery.registerMock('../../common/lib/platformUtil', fakePlatformUtil) windowsReducer = require('../../../../../app/browser/reducers/windowsReducer') }) after(function () { mockery.disable() + fakeTimers.restore() }) - describe('APP_WINDOW_UPDATED', function () { + describe('APP_NEW_WINDOW', function () { let spy + before(function () { + spy = sinon.spy(fakeWindowApi, 'createWindow') + }) + afterEach(function () { + spy.reset() + }) + after(function () { + spy.restore() + }) + const sampleFrame1 = { + location: 'http://mysite.com' + } + const sampleFrame2 = { + location: 'http://mysite2.com' + } + + it('creates a window, with a single specified frame', function () { + const action = { + actionType: appConstants.APP_NEW_WINDOW, + frameOpts: sampleFrame1 + } + windowsReducer(state, action) + fakeTimers.tick(0) + // ensure the window api was asked to create the frame + const actualCreateWindowFrameArg = spy.args[0][3] + assert.deepEqual(actualCreateWindowFrameArg, [ sampleFrame1 ]) + }) + + it('creates a window, with multiple specified frames', function () { + const action = { + actionType: appConstants.APP_NEW_WINDOW, + frameOpts: [ sampleFrame1, sampleFrame2 ] + } + windowsReducer(state, action) + fakeTimers.tick(0) + // ensure the window api was asked to create the frame + const actualCreateWindowFrameArg = spy.args[0][3] + assert.deepEqual(actualCreateWindowFrameArg, [ sampleFrame1, sampleFrame2 ]) + }) + + it('creates a window taking up the entire screen workarea by default', function () { + const fakeDisplay = new FakeElectronDisplay() + const action = { + actionType: appConstants.APP_NEW_WINDOW, + frameOpts: [ sampleFrame1, sampleFrame2 ] + } + windowsReducer(state, action) + fakeTimers.tick(0) + // ensure the window api was asked to create the frame + const { width, height } = spy.args[0][0] + assert.deepEqual(fakeDisplay.workAreaSize, { width, height }) + }) + + it('allows a window size to be exactly specified', function () { + const expectedDimensions = { width: 600, outerHeight: 700 } + const action = { + actionType: appConstants.APP_NEW_WINDOW, + browserOpts: Object.assign({}, expectedDimensions) + } + windowsReducer(state, action) + fakeTimers.tick(0) + const windowOptions = spy.args[0][0] + assert.propertyVal(windowOptions, 'width', expectedDimensions.width) + assert.propertyVal(windowOptions, 'height', expectedDimensions.outerHeight) + }) + + it('allows a window size to be specified, ignoring navBar height', function () { + const expectedDimensions = { width: 600, height: 700 } + const action = { + actionType: appConstants.APP_NEW_WINDOW, + browserOpts: Object.assign({}, expectedDimensions) + } + windowsReducer(state, action) + fakeTimers.tick(0) + const windowOptions = spy.args[0][0] + // width should be exact + assert.propertyVal(windowOptions, 'width', expectedDimensions.width) + // height should have 'navBar' added on to it + assert.isAbove(windowOptions.height, expectedDimensions.height) + // but should not be larger than screen height + assert.isBelow(windowOptions.height, new FakeElectronDisplay().workAreaSize.height) + }) + + it('positions the window by the mouse cursor when asked', function () { + const expectedPosition = fakeElectron.screen.getCursorScreenPoint() + const action = { + actionType: appConstants.APP_NEW_WINDOW, + browserOpts: { positionByMouseCursor: true } + } + windowsReducer(state, action) + fakeTimers.tick(0) + const windowOptions = spy.args[0][0] + assert.propertyVal(windowOptions, 'x', expectedPosition.x) + assert.propertyVal(windowOptions, 'y', expectedPosition.y) + }) + + it('positions the window to an exact point when asked', function () { + const expectedPosition = { x: 500, y: 600 } + const action = { + actionType: appConstants.APP_NEW_WINDOW, + browserOpts: { + x: expectedPosition.x, + y: expectedPosition.y + } + } + windowsReducer(state, action) + fakeTimers.tick(0) + const windowOptions = spy.args[0][0] + assert.propertyVal(windowOptions, 'x', expectedPosition.x) + assert.propertyVal(windowOptions, 'y', expectedPosition.y) + }) + + it('restores a maximized window', function () { + const action = { + actionType: appConstants.APP_NEW_WINDOW, + restoredState: { + windowInfo: { state: 'maximized' } + } + } + windowsReducer(state, action) + fakeTimers.tick(0) + const actualIsMaximized = spy.args[0][2] + assert.isTrue(actualIsMaximized) + }) + + it('does not maximize a window by default', function () { + const action = { + actionType: appConstants.APP_NEW_WINDOW + } + windowsReducer(state, action) + fakeTimers.tick(0) + const actualIsMaximized = spy.args[0][2] + assert.isFalse(actualIsMaximized) + }) + }) + + describe('APP_WINDOW_UPDATED', function () { + let spy + before(function () { + spy = sinon.spy(fakeWindowState, 'maybeCreateWindow') + }) afterEach(function () { + spy.reset() + }) + after(function () { spy.restore() }) it('null case', function () { - spy = sinon.spy(fakeWindowState, 'maybeCreateWindow') const newState = windowsReducer(state, { actionType: appConstants.APP_WINDOW_UPDATED }) @@ -58,7 +215,6 @@ describe('windowsReducer unit test', function () { }) it('updateDefault is false (we shouldnt update it)', function () { - spy = sinon.spy(fakeWindowState, 'maybeCreateWindow') const newState = windowsReducer(state, { actionType: appConstants.APP_WINDOW_UPDATED, updateDefault: false @@ -68,7 +224,6 @@ describe('windowsReducer unit test', function () { }) it('updateDefault is true', function () { - spy = sinon.spy(fakeWindowState, 'maybeCreateWindow') const newState = windowsReducer(state, { actionType: appConstants.APP_WINDOW_UPDATED, updateDefault: true, diff --git a/test/unit/app/browser/windowsTest.js b/test/unit/app/browser/windowsTest.js index bec84f1cd82..a49b8b4b724 100644 --- a/test/unit/app/browser/windowsTest.js +++ b/test/unit/app/browser/windowsTest.js @@ -2,16 +2,33 @@ const mockery = require('mockery') const sinon = require('sinon') const Immutable = require('immutable') -const assert = require('assert') +const { assert } = require('chai') const fakeElectron = require('../../lib/fakeElectron') +const FakeWindow = require('../../lib/fakeWindow') const fakeAdBlock = require('../../lib/fakeAdBlock') +const fakePlatformUtil = { + isDarwin: () => true, + isWindows: () => false +} + +const fakeAppDispatcher = { + registerWindow: () => { + } +} + require('../../braveUnit') describe('window API unit tests', function () { let windows, appActions let appStore let defaultState, createTabState, tabCloseState + const windowCreateTimeout = 5000 + let browserWindowSpy + let browserShowSpy + let fakeTimers + let setFullscreenSpy + let maximizeSpy before(function () { mockery.enable({ @@ -66,7 +83,8 @@ describe('window API unit tests', function () { mockery.registerMock('electron', fakeElectron) mockery.registerMock('ad-block', fakeAdBlock) mockery.registerMock('../../js/stores/appStore', appStore) - + mockery.registerMock('../../common/lib/platformUtil', fakePlatformUtil) + mockery.registerMock('../../js/dispatcher/appDispatcher', fakeAppDispatcher) windows = require('../../../../app/browser/windows') appActions = require('../../../../js/actions/appActions') }) @@ -75,6 +93,33 @@ describe('window API unit tests', function () { mockery.disable() }) + // BrowserWindow related hooks + before(function () { + browserShowSpy = sinon.spy(FakeWindow.prototype, 'show') + browserWindowSpy = sinon.spy(fakeElectron, 'BrowserWindow') + setFullscreenSpy = sinon.spy(FakeWindow.prototype, 'setFullScreen') + maximizeSpy = sinon.spy(FakeWindow.prototype, 'maximize') + }) + + beforeEach(function () { + fakeTimers = sinon.useFakeTimers() + }) + + after(function () { + browserWindowSpy.restore() + browserShowSpy.restore() + setFullscreenSpy.restore() + maximizeSpy.restore() + }) + + afterEach(function () { + fakeTimers.restore() + browserWindowSpy.reset() + browserShowSpy.reset() + setFullscreenSpy.reset() + maximizeSpy.reset() + }) + describe('privateMethods', function () { let updatePinnedTabs let createTabRequestedSpy, tabCloseRequestedSpy @@ -121,5 +166,119 @@ describe('window API unit tests', function () { assert.equal(tabCloseRequestedSpy.calledOnce, true) }) }) + + describe('createWindow', function () { + describe('show window immediately', function () { + it('creates a window immediately visible, when asked not to hide until render', function () { + windows.createWindow({ }, null, false, null, Immutable.Map(), false) + const windowOptions = browserWindowSpy.args[0][0] + assert.equal(browserWindowSpy.callCount, 1) + // BrowserWindow ctor options.show is true by default + // so make sure we're not passing in false + assert.isNotFalse(windowOptions.show) + }) + it('maximizes the window, when specified', function () { + windows.createWindow({ }, null, true, null, Immutable.Map(), false) + // check if window is made fullscreen + assert.propertyVal(maximizeSpy, 'callCount', 1) + }) + }) + + describe('hide window until render', function () { + it('creates a window hidden at first', function () { + windows.createWindow({ }, null, false, null, Immutable.Map(), true) + fakeTimers.tick(windowCreateTimeout) + assert.equal(browserWindowSpy.callCount, 1) + const windowOptions = browserWindowSpy.args[0][0] + assert.isFalse(windowOptions.show) + }) + + it('shows the window after a timeout', function () { + windows.createWindow({ }, null, false, null, Immutable.Map(), true) + assert.equal(browserWindowSpy.callCount, 1) + assert.equal(browserShowSpy.callCount, 0) + fakeTimers.tick(windowCreateTimeout) + assert.equal(browserShowSpy.callCount, 1) + }) + + it('replicates macOS functionality by creating a fullscreen window from a parent fullscreen window', function () { + const parentWindow = new FakeWindow() + parentWindow.isFullScreen = () => true + windows.createWindow({ }, parentWindow, false, null, Immutable.Map(), true) + // allow the window to be created after timeout + const windowOptions = browserWindowSpy.args[0][0] + // should not ask OS to go fullscreen when window isn't shown yet + assert.isNotTrue(windowOptions.fullscreen) + assert.equal(browserWindowSpy.callCount, 1) + // should store that the window should go fullscreen when rendered + assert.isObject(browserWindowSpy.returnValues[0]) + assert.propertyVal(browserWindowSpy.returnValues[0], '__shouldFullscreen', true) + }) + }) + }) + + describe('windowRendered', function () { + it('shows the window if it is not visible', function () { + // create a window that is set to show on render + const win = windows.createWindow({ }, null, false, null, Immutable.Map(), true) + assert.equal(browserWindowSpy.callCount, 1) + // make sure window has not been shown + const windowOptions = browserWindowSpy.args[0][0] + assert.isFalse(windowOptions.show) + assert.equal(browserShowSpy.callCount, 0) + // a little time elapsed, but not enough to timeout window showing + fakeTimers.tick(Math.floor(windowCreateTimeout / 2)) + // make sure window has not yet been shown + assert.equal(browserShowSpy.callCount, 0) + // notify rendered + windows.windowRendered(win) + // windowRendered schedules on setImmediate + fakeTimers.tick(0) + // check if window is shown + assert.propertyVal(browserShowSpy, 'callCount', 1) + }) + + it('makes the window fullscreen if specified', function () { + const parentWindow = new FakeWindow() + parentWindow.isFullScreen = () => true + // create a window that is set to show on render + const win = windows.createWindow({ }, parentWindow, false, null, Immutable.Map(), true) + assert.equal(browserWindowSpy.callCount, 1) + // make sure window has not been shown + const windowOptions = browserWindowSpy.args[0][0] + assert.isFalse(windowOptions.show) + assert.equal(browserShowSpy.callCount, 0) + // a little time elapsed, but not enough to timeout window showing + fakeTimers.tick(Math.floor(windowCreateTimeout / 2)) + assert.propertyVal(setFullscreenSpy, 'callCount', 0) + // notify rendered + windows.windowRendered(win) + // windowRendered schedules on setImmediate + // setfullscreen performs action after 100ms timeout + fakeTimers.tick(100) + // check if window is made fullscreen + assert.propertyVal(setFullscreenSpy, 'callCount', 1) + }) + + it('makes the window maximized if specified', function () { + // create a window that is set to show on render + const win = windows.createWindow({ }, null, true, null, Immutable.Map(), true) + assert.equal(browserWindowSpy.callCount, 1) + // make sure window has not been shown + const windowOptions = browserWindowSpy.args[0][0] + assert.isFalse(windowOptions.show) + assert.equal(browserShowSpy.callCount, 0) + // a little time elapsed, but not enough to timeout window showing + fakeTimers.tick(Math.floor(windowCreateTimeout / 2)) + assert.propertyVal(maximizeSpy, 'callCount', 0) + // notify rendered + windows.windowRendered(win) + // windowRendered schedules on setImmediate + fakeTimers.tick(0) + // windowRendered schedules on setImmediate + // check if window is made fullscreen + assert.propertyVal(maximizeSpy, 'callCount', 1) + }) + }) }) }) diff --git a/test/unit/lib/fakeElectron.js b/test/unit/lib/fakeElectron.js index f33b52f7dcc..82faf8d533b 100644 --- a/test/unit/lib/fakeElectron.js +++ b/test/unit/lib/fakeElectron.js @@ -1,4 +1,6 @@ const {EventEmitter} = require('events') +const FakeElectronDisplay = require('./fakeElectronDisplay') +const FakeElectronWindow = require('./fakeWindow') const ipcMain = new EventEmitter() ipcMain.send = ipcMain.emit const fakeElectron = { @@ -7,21 +9,7 @@ const fakeElectron = { fakeElectron.remote.app.removeAllListeners() fakeElectron.autoUpdater.removeAllListeners() }, - BrowserWindow: { - getFocusedWindow: function () { - return { - id: 1 - } - }, - getActiveWindow: function () { - return { - id: 1 - } - }, - getAllWindows: function () { - return [{id: 1}] - } - }, + BrowserWindow: FakeElectronWindow, MenuItem: class { constructor (template) { this.template = template @@ -100,7 +88,16 @@ const fakeElectron = { autoUpdater: new EventEmitter(), importer: { on: () => {} + }, + screen: { + getDisplayMatching: () => new FakeElectronDisplay(), + getPrimaryDisplay: () => new FakeElectronDisplay(), + getDisplayNearestPoint: () => new FakeElectronDisplay(), + getAllDisplays: () => [new FakeElectronDisplay()], + getCursorScreenPoint: () => ({ x: 200, y: 200 }) } } +// function getFakeDisplay + module.exports = fakeElectron diff --git a/test/unit/lib/fakeElectronDisplay.js b/test/unit/lib/fakeElectronDisplay.js new file mode 100644 index 00000000000..a774dc2b762 --- /dev/null +++ b/test/unit/lib/fakeElectronDisplay.js @@ -0,0 +1,25 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +class FakeElectronDisplay { + constructor () { + this.id = 1 + this.bounds = { + x: 0, + y: 0, + width: 1280, + height: 1100 + } + this.size = { + width: 1280, + height: 1100 + } + this.workAreaSize = { + width: 1100, + height: 1000 + } + } +} + +module.exports = FakeElectronDisplay diff --git a/test/unit/lib/fakeWindow.js b/test/unit/lib/fakeWindow.js index be694a57749..ed08fc3245f 100644 --- a/test/unit/lib/fakeWindow.js +++ b/test/unit/lib/fakeWindow.js @@ -3,17 +3,65 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ const EventEmitter = require('events') +const util = require('util') -class FakeWindow extends EventEmitter { - constructor (id) { - super() - this.id = id - this.webContents = Object.assign(new EventEmitter()) - this.webContents.send = this.webContents.emit - } - getId () { - return this.id +// cannot be a class since sinon has +// trouble stubbing the constructtor for a class +function FakeWindow (id) { + this.id = id + this.webContents = Object.assign(new EventEmitter()) + this.webContents.send = this.webContents.emit + this._isVisible = false +} + +util.inherits(FakeWindow, EventEmitter) + +// +// instance functions +// + +FakeWindow.prototype.getId = function () { + return this.id +} +FakeWindow.prototype.getBounds = function () { + return { + x: 10, + y: 10, + width: 800, + height: 600 } } +FakeWindow.prototype.isDestroyed = function () { + return false +} +FakeWindow.prototype.loadURL = function (url) { } +FakeWindow.prototype.show = function () { + this._isVisible = true +} +FakeWindow.prototype.hide = function () { + this._isVisible = false +} +FakeWindow.prototype.setFullScreen = function () { } +FakeWindow.prototype.maximize = function () { } +FakeWindow.prototype.isVisible = function () { + return this._isVisible +} + +// +// static functions +// + +FakeWindow.getFocusedWindow = function () { + return new FakeWindow(1) +} +FakeWindow.getActiveWindow = function () { + return new FakeWindow(1) +} +FakeWindow.getAllWindows = function () { + return [new FakeWindow(1)] +} +FakeWindow.fromWebContents = function () { + return new FakeWindow(1) +} module.exports = FakeWindow