From 1fd26129362da38b4ee2cf41ac4449cc383b2568 Mon Sep 17 00:00:00 2001 From: bridiver Date: Wed, 24 Aug 2016 16:18:02 -0700 Subject: [PATCH] move basic auth state to appStore fixes #3354 --- app/browser/basicAuth.js | 61 +++++ app/common/state/basicAuthState.js | 41 +++ app/common/state/immutableUtil.js | 16 ++ app/common/state/tabState.js | 69 +++++ app/index.js | 29 +- app/sessionStore.js | 7 + docs/appActions.md | 12 + docs/windowActions.md | 12 - js/actions/appActions.js | 21 ++ js/actions/windowActions.js | 17 -- js/components/loginRequired.js | 20 +- js/components/main.js | 16 +- js/constants/appConstants.js | 4 +- js/constants/messages.js | 2 - js/constants/windowConstants.js | 1 - js/stores/appStore.js | 14 + js/stores/windowStore.js | 7 - test/unit/common/state/basicAuthStateTest.js | 175 ++++++++++++ test/unit/common/state/tabStateTest.js | 269 +++++++++++++++++++ 19 files changed, 703 insertions(+), 90 deletions(-) create mode 100644 app/browser/basicAuth.js create mode 100644 app/common/state/basicAuthState.js create mode 100644 app/common/state/immutableUtil.js create mode 100644 app/common/state/tabState.js create mode 100644 test/unit/common/state/basicAuthStateTest.js create mode 100644 test/unit/common/state/tabStateTest.js diff --git a/app/browser/basicAuth.js b/app/browser/basicAuth.js new file mode 100644 index 00000000000..ad68ed73283 --- /dev/null +++ b/app/browser/basicAuth.js @@ -0,0 +1,61 @@ +const electron = require('electron') +const app = electron.app +const appActions = require('../../js/actions/appActions') +const appConstants = require('../../js/constants/appConstants') +const appDispatcher = require('../../js/dispatcher/appDispatcher') +const appStore = require('../../js/stores/appStore') + +// URLs to callback for auth. +let authCallbacks = {} + +const cleanupAuthCallback = (tabId) => { + delete authCallbacks[tabId] +} + +const runAuthCallback = (tabId, detail) => { + let cb = authCallbacks[tabId] + if (cb) { + delete authCallbacks[tabId] + if (detail) { + let username = detail.get('username') + let password = detail.get('password') + cb(username, password) + } else { + cb() + } + } +} + +const doAction = (action) => { + switch (action.actionType) { + case appConstants.APP_SET_LOGIN_RESPONSE_DETAIL: + appDispatcher.waitFor([appStore.dispatchToken], () => { + runAuthCallback(action.tabId, action.detail) + }) + break + default: + } +} + +const basicAuth = { + init: () => { + appDispatcher.register(doAction) + app.on('login', (e, webContents, request, authInfo, cb) => { + e.preventDefault() + let tabId = webContents.getId() + authCallbacks[tabId] = cb + webContents.on('destroyed', () => { + cleanupAuthCallback(tabId) + }) + webContents.on('crashed', () => { + cleanupAuthCallback(tabId) + }) + appActions.setLoginRequiredDetail(tabId, { + request, + authInfo + }) + }) + } +} + +module.exports = basicAuth diff --git a/app/common/state/basicAuthState.js b/app/common/state/basicAuthState.js new file mode 100644 index 00000000000..fdfb87990a3 --- /dev/null +++ b/app/common/state/basicAuthState.js @@ -0,0 +1,41 @@ +/* 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/. */ + +const tabState = require('./tabState') +const { makeImmutable } = require('./immutableUtil') + +const loginRequiredDetail = 'loginRequiredDetail' +tabState.addTransientFields([loginRequiredDetail]) + +const basicAuthState = { + setLoginRequiredDetail: (appState, tabId, detail) => { + appState = makeImmutable(appState) + detail = makeImmutable(detail) + let tab = tabState.getOrCreateByTabId(appState, tabId) + if (!detail || detail.size === 0) { + tab = tab.delete(loginRequiredDetail) + } else { + tab = tab.set(loginRequiredDetail, detail) + } + return tabState.updateTab(appState, tabId, tab) + }, + + getLoginRequiredDetail: (appState, tabId) => { + appState = makeImmutable(appState) + let tab = tabState.getByTabId(appState, tabId) + return tab && tab.get(loginRequiredDetail) + }, + + setLoginResponseDetail: (appState, tabId, detail) => { + appState = makeImmutable(appState) + let tab = tabState.getByTabId(appState, tabId) + if (!tab) { + return appState + } + tab = tab.delete(loginRequiredDetail) + return tabState.updateTab(appState, tabId, tab) + } +} + +module.exports = basicAuthState diff --git a/app/common/state/immutableUtil.js b/app/common/state/immutableUtil.js new file mode 100644 index 00000000000..f4edd4c8f75 --- /dev/null +++ b/app/common/state/immutableUtil.js @@ -0,0 +1,16 @@ +/* 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/. */ + +const Immutable = require('immutable') + +const immutableUtils = { + makeImmutable: (obj) => { + if (!obj) { + return null + } + return obj.toJS ? obj : Immutable.fromJS(obj) + } +} + +module.exports = immutableUtils diff --git a/app/common/state/tabState.js b/app/common/state/tabState.js new file mode 100644 index 00000000000..7836dce7629 --- /dev/null +++ b/app/common/state/tabState.js @@ -0,0 +1,69 @@ +/* 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/. */ + +const { makeImmutable } = require('./immutableUtil') + +let transientFields = ['tabId', 'windowId'] + +const tabState = { + defaultTabState: makeImmutable({ + windowId: -1, + frameKey: -1, + tabId: -1 + }), + + getTabIndexByTabId: (state, tabId) => { + return state.get('tabs').findIndex((tab) => tab.get('tabId') === tabId) + }, + + createTab: (props) => { + props = makeImmutable(props) + return tabState.defaultTabState.merge(props) + }, + + getOrCreateByTabId: (state, tabId) => { + let tab = tabState.getByTabId(state, tabId) + return tab || tabState.createTab({tabId}) + }, + + getByTabId: (state, tabId) => { + return state.get('tabs').find((tab) => tab.get('tabId') === tabId) + }, + + closeTab: (state, tabId) => { + let index = tabState.getTabIndexByTabId(state, tabId) + if (index === -1) { + return state + } + + let tabs = state.get('tabs').delete(index) + state = state.set('tabs', tabs) + return state + }, + + updateTab: (state, tabId, tab) => { + let tabs = state.get('tabs') + let index = tabState.getTabIndexByTabId(state, tabId) + tabs = tabs.delete(index).insert(index, tab) + return state.set('tabs', tabs) + }, + + addTransientFields: (fields) => { + transientFields = transientFields.concat(fields) + }, + + getTransientFields: () => { + return transientFields + }, + + getPersistentTabState: (tab) => { + tab = makeImmutable(tab) + tabState.getTransientFields().forEach((field) => { + tab = tab.delete(field) + }) + return tab + } +} + +module.exports = tabState diff --git a/app/index.js b/app/index.js index cc9870e8eb9..25717675c7d 100644 --- a/app/index.js +++ b/app/index.js @@ -62,6 +62,7 @@ const ledger = require('./ledger') const flash = require('../js/flash') const contentSettings = require('../js/state/contentSettings') const privacy = require('../js/state/privacy') +const basicAuth = require('./browser/basicAuth') // Used to collect the per window state when shutting down the application let perWindowState = [] @@ -73,8 +74,6 @@ let lastWindowClosed = false // Domains to accept bad certs for. TODO: Save the accepted cert fingerprints. let acceptCertDomains = {} let errorCerts = {} -// URLs to callback for auth. -let authCallbacks = {} // Don't show the keytar prompt more than once per 24 hours let throttleKeytar = false @@ -252,16 +251,6 @@ app.on('ready', () => { }) }) - app.on('login', (e, webContents, request, authInfo, cb) => { - e.preventDefault() - authCallbacks[request.url] = cb - let sender = webContents.hostWebContents || webContents - sender.send(messages.LOGIN_REQUIRED, { - url: request.url, - tabId: webContents.getId(), - authInfo - }) - }) app.on('window-all-closed', () => { // On macOS it is common for applications and their menu bar // to stay active until the user quits explicitly with Cmd + Q @@ -354,21 +343,6 @@ app.on('ready', () => { return event.returnValue }) - ipcMain.on(messages.LOGIN_RESPONSE, (e, url, username, password) => { - if (username || password) { - // Having 2 of the same tab URLs open right now, where both require auth - // can cause an error / alert here. Ignore it for now. - try { - if (authCallbacks[url]) { - authCallbacks[url](username, password) - } - } catch (e) { - console.error(e) - } - } - delete authCallbacks[url] - }) - process.on(messages.UNDO_CLOSED_WINDOW, () => { if (lastWindowState) { appActions.newWindow(undefined, undefined, lastWindowState) @@ -392,6 +366,7 @@ app.on('ready', () => { appActions.setState(Immutable.fromJS(initialState)) return loadedPerWindowState }).then((loadedPerWindowState) => { + basicAuth.init() contentSettings.init() privacy.init() Extensions.init() diff --git a/app/sessionStore.js b/app/sessionStore.js index 44885cb0b95..6f37855512d 100644 --- a/app/sessionStore.js +++ b/app/sessionStore.js @@ -21,6 +21,7 @@ const downloadStates = require('../js/constants/downloadStates') const {tabFromFrame} = require('../js/state/frameStateUtil') const sessionStorageVersion = 1 const filtering = require('./filtering') +// const tabState = require('./common/state/tabState') let suffix = '' if (process.env.NODE_ENV === 'development') { @@ -274,6 +275,11 @@ module.exports.cleanAppData = (data, isShutdown) => { }) } } + // all data in tabs is transient while we make the transition from window to app state + delete data.tabs + // if (data.tabs) { + // data.tabs = data.tabs.map((tab) => tabState.getPersistentTabState(tab).toJS()) + // } } /** @@ -356,6 +362,7 @@ module.exports.loadAppState = () => { module.exports.defaultAppState = () => { return { sites: [], + tabs: [], visits: [], settings: {}, siteSettings: {}, diff --git a/docs/appActions.md b/docs/appActions.md index 48006a8d4d0..49dcd22d318 100644 --- a/docs/appActions.md +++ b/docs/appActions.md @@ -312,6 +312,18 @@ Adds a word to the dictionary +### setLoginRequiredDetail(tabId, detail) + +Adds information about pending basic auth login requests + +**Parameters** + +**tabId**: `number`, The tabId that generated the request + +**detail**: `string`, login request info + + + ### clearAppData(clearDataDetail) Clears the data specified in dataDetail diff --git a/docs/windowActions.md b/docs/windowActions.md index a5057d0d68a..86c8aeba492 100644 --- a/docs/windowActions.md +++ b/docs/windowActions.md @@ -88,18 +88,6 @@ Dispatches a message to set the frame error state -### setLoginRequiredDetail(frameProps, detail) - -Dispatches a message to set the login required detail. - -**Parameters** - -**frameProps**: `Object`, The frame where the login required prompt should be shown. - -**detail**: `Object`, Details of the login required operation. - - - ### setNavBarUserInput(location) Dispatches a message to the store to set the user entered text for the URL bar. diff --git a/js/actions/appActions.js b/js/actions/appActions.js index 4145a3af08c..3e5524eee9e 100644 --- a/js/actions/appActions.js +++ b/js/actions/appActions.js @@ -358,6 +358,27 @@ const appActions = { }) }, + /** + * Adds information about pending basic auth login requests + * @param {number} tabId - The tabId that generated the request + * @param {string} detail - login request info + */ + setLoginRequiredDetail: function (tabId, detail) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_SET_LOGIN_REQUIRED_DETAIL, + tabId, + detail + }) + }, + + setLoginResponseDetail: function (tabId, detail) { + AppDispatcher.dispatch({ + actionType: AppConstants.APP_SET_LOGIN_RESPONSE_DETAIL, + tabId, + detail + }) + }, + /** * Clears the data specified in dataDetail * @param {object} clearDataDetail - the app data to clear as per doc/state.md's clearBrowsingDataDetail diff --git a/js/actions/windowActions.js b/js/actions/windowActions.js index e9fd78dd8e8..2da0e796c76 100644 --- a/js/actions/windowActions.js +++ b/js/actions/windowActions.js @@ -137,19 +137,6 @@ const windowActions = { }) }, - /** - * Dispatches a message to set the login required detail. - * @param {Object} frameProps - The frame where the login required prompt should be shown. - * @param {Object} detail - Details of the login required operation. - */ - setLoginRequiredDetail: function (frameProps, detail) { - dispatch({ - actionType: WindowConstants.WINDOW_SET_LOGIN_REQUIRED_DETAIL, - frameProps, - detail - }) - }, - /** * Dispatches a message to the store to set the user entered text for the URL bar. * Unlike setLocation and loadUrl, this does not modify the state of src and location. @@ -324,10 +311,6 @@ const windowActions = { webviewActions.setFullScreen(false) this.setFullScreen(frameProps, false) } - // Flush out any pending login required prompts - if (frameProps && frameProps.getIn(['security', 'loginRequiredDetail'])) { - ipc.send(messages.LOGIN_RESPONSE, frameProps.get('location')) - } // Unless a caller explicitly specifies to close a pinned frame, then // ignore the call. const nonPinnedFrames = frames.filter((frame) => !frame.get('pinnedLocation')) diff --git a/js/components/loginRequired.js b/js/components/loginRequired.js index 0b065936059..04ee7328055 100644 --- a/js/components/loginRequired.js +++ b/js/components/loginRequired.js @@ -5,11 +5,8 @@ const React = require('react') const Dialog = require('./dialog') const Button = require('./button') -const windowActions = require('../actions/windowActions') +const appActions = require('../actions/appActions') const KeyCodes = require('../constants/keyCodes') -const messages = require('../constants/messages') -const electron = global.require('electron') -const ipc = electron.ipcRenderer const url = require('url') class LoginRequired extends React.Component { @@ -32,7 +29,10 @@ class LoginRequired extends React.Component { this.focus() } get detail () { - return this.props.frameProps.getIn(['security', 'loginRequiredDetail']) + return this.props.loginRequiredDetail + } + get tabId () { + return this.props.tabId } onKeyDown (e) { switch (e.keyCode) { @@ -45,10 +45,7 @@ class LoginRequired extends React.Component { } } onClose () { - const location = this.props.frameProps.get('location') - ipc.send(messages.LOGIN_RESPONSE, location) - windowActions.setLoginRequiredDetail(this.props.frameProps) - windowActions.onWebviewLoadEnd(this.props.frameProps) + appActions.setLoginResponseDetail(this.tabId) } onClick (e) { e.stopPropagation() @@ -69,12 +66,11 @@ class LoginRequired extends React.Component { username: '', password: '' }) - ipc.send(messages.LOGIN_RESPONSE, this.detail.url, this.state.username, this.state.password) - windowActions.setLoginRequiredDetail(this.props.frameProps) + appActions.setLoginResponseDetail(this.tabId, this.state) } render () { const l10nArgs = { - host: url.resolve(this.props.frameProps.get('location'), '/') + host: url.resolve(this.detail.getIn(['request', 'url']), '/') } return
diff --git a/js/components/main.js b/js/components/main.js index 16e8887231d..d8eb39d1ccf 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -48,8 +48,8 @@ const dragTypes = require('../constants/dragTypes') const keyCodes = require('../constants/keyCodes') // State handling +const basicAuthState = require('../../app/common/state/basicAuthState') const FrameStateUtil = require('../state/frameStateUtil') - const searchProviders = require('../data/searchProviders') // Util @@ -367,13 +367,6 @@ class Main extends ImmutableComponent { securityState) }) - ipc.on(messages.LOGIN_REQUIRED, (e, detail) => { - const frame = FrameStateUtil.getFrameByTabId(self.props.windowState, detail.tabId) - if (frame) { - windowActions.setLoginRequiredDetail(frame, detail) - } - }) - ipc.on(messages.SHOW_USERNAME_LIST, (e, usernames, origin, action, boundingRect) => { const topOffset = this.tabContainer.getBoundingClientRect().top contextMenus.onShowUsernameMenu(usernames, origin, action, boundingRect, topOffset) @@ -701,6 +694,7 @@ class Main extends ImmutableComponent { const noScriptIsVisible = this.props.windowState.getIn(['ui', 'noScriptInfo', 'isVisible']) const releaseNotesIsVisible = this.props.windowState.getIn(['ui', 'releaseNotes', 'isVisible']) const braverySettings = siteSettings.activeSettings(activeSiteSettings, this.props.appState, appConfig) + const loginRequiredDetail = activeFrame ? basicAuthState.getLoginRequiredDetail(this.props.appState, activeFrame.get('tabId')) : null const shouldAllowWindowDrag = !this.props.windowState.get('contextMenuDetail') && !this.props.windowState.get('bookmarkDetail') && @@ -817,9 +811,9 @@ class Main extends ImmutableComponent { : null } { - activeFrame && activeFrame.getIn(['security', 'loginRequiredDetail']) - ? - : null + loginRequiredDetail + ? + : null } { this.props.windowState.get('bookmarkDetail') diff --git a/js/constants/appConstants.js b/js/constants/appConstants.js index da69d31ab5a..abcdd31ab38 100644 --- a/js/constants/appConstants.js +++ b/js/constants/appConstants.js @@ -38,7 +38,9 @@ const AppConstants = { APP_ADD_AUTOFILL_ADDRESS: _, APP_REMOVE_AUTOFILL_ADDRESS: _, APP_ADD_AUTOFILL_CREDIT_CARD: _, - APP_REMOVE_AUTOFILL_CREDIT_CARD: _ + APP_REMOVE_AUTOFILL_CREDIT_CARD: _, + APP_SET_LOGIN_REQUIRED_DETAIL: _, + APP_SET_LOGIN_RESPONSE_DETAIL: _ } module.exports = mapValuesByKeys(AppConstants) diff --git a/js/constants/messages.js b/js/constants/messages.js index 36e39f8ce88..81208a55efa 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -47,8 +47,6 @@ const messages = { OPEN_BRAVERY_PANEL: _, PREFS_RESTART: _, CERT_ERROR: _, /** @arg {Object} details of certificate error */ - LOGIN_REQUIRED: _, /** @arg {Object} details of the login required request */ - LOGIN_RESPONSE: _, NOTIFICATION_RESPONSE: _, /** @arg {string} message, @arg {number} buttonId, @arg {boolean} persist */ // Downloads SHOW_DOWNLOADS_TOOLBAR: _, /** Ensures the downloads toolbar is visible */ diff --git a/js/constants/windowConstants.js b/js/constants/windowConstants.js index 0b1a02e8d74..fd537128644 100644 --- a/js/constants/windowConstants.js +++ b/js/constants/windowConstants.js @@ -62,7 +62,6 @@ const windowConstants = { WINDOW_SET_BLOCKED_BY: _, // Whether or not to show site info like # of blocked ads WINDOW_SET_REDIRECTED_BY: _, // Whether or not to show site info like redirected resources WINDOW_SET_SECURITY_STATE: _, - WINDOW_SET_LOGIN_REQUIRED_DETAIL: _, WINDOW_SET_STATE: _, WINDOW_SET_LAST_ZOOM_PERCENTAGE: _, WINDOW_SET_CLEAR_BROWSING_DATA_DETAIL: _, diff --git a/js/stores/appStore.js b/js/stores/appStore.js index 53aececae4d..c6659d689b7 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -4,6 +4,7 @@ 'use strict' const AppConstants = require('../constants/appConstants') +const WindowConstants = require('../constants/windowConstants') const AppDispatcher = require('../dispatcher/appDispatcher') const appConfig = require('../constants/appConfig') const settings = require('../constants/settings') @@ -30,6 +31,10 @@ const isDarwin = process.platform === 'darwin' const locale = require('../../app/locale') const path = require('path') +// state helpers +const basicAuthState = require('../../app/common/state/basicAuthState') +const tabState = require('../../app/common/state/tabState') + // Only used internally const CHANGE_EVENT = 'app-state-change' @@ -600,6 +605,15 @@ const handleAppAction = (action) => { Filtering.removeAutofillCreditCard(action.detail.guid) break } + case AppConstants.APP_SET_LOGIN_REQUIRED_DETAIL: + appState = basicAuthState.setLoginRequiredDetail(appState, action.tabId, action.detail) + break + case AppConstants.APP_SET_LOGIN_RESPONSE_DETAIL: + appState = basicAuthState.setLoginResponseDetail(appState, action.tabId, action.detail) + break + case WindowConstants.WINDOW_CLOSE_FRAME: + appState = tabState.closeTab(appState, action.frameProps.get('tabId')) + break default: } diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index acc9585c75c..969c8179275 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -745,13 +745,6 @@ const doAction = (action) => { action.securityState.certDetails) } break - case WindowConstants.WINDOW_SET_LOGIN_REQUIRED_DETAIL: - if (action.detail) { - windowState = windowState.setIn(frameStatePathForFrame(action.frameProps).concat(['security', 'loginRequiredDetail']), action.detail) - } else { - windowState = windowState.deleteIn(frameStatePathForFrame(action.frameProps).concat(['security', 'loginRequiredDetail'])) - } - break case WindowConstants.WINDOW_SET_BLOCKED_BY: const blockedByPath = ['frames', FrameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), action.blockType, 'blocked'] let blockedBy = windowState.getIn(blockedByPath) || new Immutable.List() diff --git a/test/unit/common/state/basicAuthStateTest.js b/test/unit/common/state/basicAuthStateTest.js new file mode 100644 index 00000000000..21b07203594 --- /dev/null +++ b/test/unit/common/state/basicAuthStateTest.js @@ -0,0 +1,175 @@ +/* global describe, it, before */ +const basicAuthState = require('../../../../app/common/state/basicAuthState') +const tabState = require('../../../../app/common/state/tabState') +const Immutable = require('immutable') +const assert = require('assert') + +const defaultAppState = Immutable.fromJS({ + tabs: [] +}) + +const defaultTab = Immutable.fromJS({ + tabId: 1, + loginRequiredDetail: { + request: { url: 'someurl' }, + authInfo: { authInfoProp: 'value' } + } +}) + +describe('basicAuthState', function () { + describe('setLoginResponseDetail', function () { + describe('`tabId` exists in appState with loginRequiredDetail', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([defaultTab])) + this.appState = basicAuthState.setLoginResponseDetail(this.appState, 1, { + username: 'username', + password: 'password' + }) + }) + + it('removes the login detail', function () { + let tab = tabState.getByTabId(this.appState, 1) + assert(tab) + assert.equal(undefined, tab.get('loginRequiredDetail')) + }) + }) + + describe('`tabId` exists in appState with no loginRequiredDetail', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([{ tabId: 1 }])) + this.appState = basicAuthState.setLoginResponseDetail(this.appState, 1, { + username: 'username', + password: 'password' + }) + }) + + it('returns the unmodified appState', function () { + let tab = tabState.getByTabId(this.appState, 1) + assert(tab) + assert.equal(undefined, tab.get('loginRequiredDetail')) + }) + }) + + describe('`tabId` does not exist in appState', function () { + before(function () { + this.appState = basicAuthState.setLoginResponseDetail(defaultAppState, 1, { + username: 'username', + password: 'password' + }) + }) + + it('returns the unmodified appState', function () { + let tab = tabState.getByTabId(this.appState, 1) + assert.equal(null, tab) + }) + }) + }) + + describe('setLoginRequiredDetail', function () { + it('error for missing required fields') + + describe('with null detail', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([defaultTab])) + this.appState = basicAuthState.setLoginRequiredDetail(this.appState, 1, null) + }) + + it('removes the login detail', function () { + let tab = tabState.getByTabId(this.appState, 1) + assert(tab) + assert.equal(undefined, tab.get('loginRequiredDetail')) + }) + }) + + describe('with empty detail', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([defaultTab])) + this.appState = basicAuthState.setLoginRequiredDetail(this.appState, 1, {}) + }) + + it('removes the login detail', function () { + let tab = tabState.getByTabId(this.appState, 1) + assert(tab) + assert.equal(undefined, tab.get('loginRequiredDetail')) + }) + }) + + describe('`tabId` exists in appState', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + tabId: 1 + } + ])) + this.appState = basicAuthState.setLoginRequiredDetail(this.appState, 1, { + request: { url: 'someurl' }, + authInfo: { authInfoProp: 'value' } + }) + }) + + it('sets the login detail for `tabId` in the appState', function () { + let tab = tabState.getByTabId(this.appState, 1) + assert(tab) + let loginRequiredDetail = tab.get('loginRequiredDetail') + assert.equal('someurl', loginRequiredDetail.getIn(['request', 'url'])) + assert.equal('value', loginRequiredDetail.getIn(['authInfo', 'authInfoProp'])) + }) + }) + + describe('`tabId` does not exist in appState', function () { + before(function () { + this.appState = basicAuthState.setLoginRequiredDetail(defaultAppState, 1, { + request: { url: 'someurl' }, + authInfo: { authInfoProp: 'value' } + }) + }) + + it('creates a new tab in the appState and sets the login detail', function () { + let tab = tabState.getByTabId(this.appState, 1) + assert(tab) + let loginRequiredDetail = tab.get('loginRequiredDetail') + assert.equal('someurl', loginRequiredDetail.getIn(['request', 'url'])) + assert.equal('value', loginRequiredDetail.getIn(['authInfo', 'authInfoProp'])) + }) + }) + }) + + describe('getLoginRequiredDetail', function () { + describe('`tabId` exists in appState with loginRequiredDetail', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([defaultTab])) + this.loginRequiredDetail = basicAuthState.getLoginRequiredDetail(this.appState, 1) + }) + + it('returns the login detail for `tabId`', function () { + assert.equal('someurl', this.loginRequiredDetail.getIn(['request', 'url'])) + assert.equal('value', this.loginRequiredDetail.getIn(['authInfo', 'authInfoProp'])) + }) + }) + + describe('`tabId` exists in appState with no loginRequiredDetail', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + tabId: 1 + } + ])) + this.loginRequiredDetail = basicAuthState.getLoginRequiredDetail(this.appState, 1) + }) + + it('returns null', function () { + assert.equal(null, this.loginRequiredDetail) + }) + }) + + describe('`tabId` does not exist in appState', function () { + before(function () { + this.loginRequiredDetail = basicAuthState.getLoginRequiredDetail(defaultAppState, 1) + }) + + it('returns null', function () { + assert.equal(null, this.loginRequiredDetail) + }) + }) + }) +}) diff --git a/test/unit/common/state/tabStateTest.js b/test/unit/common/state/tabStateTest.js new file mode 100644 index 00000000000..cc6b0653326 --- /dev/null +++ b/test/unit/common/state/tabStateTest.js @@ -0,0 +1,269 @@ +/* global describe, it, before */ +const tabState = require('../../../../app/common/state/tabState') +const Immutable = require('immutable') +const assert = require('assert') + +const defaultAppState = Immutable.fromJS({ + tabs: [], + otherProp: true +}) + +describe('tabState', function () { + describe('createTab', function () { + it('creates a new tab from the defaultTabState', function () { + let tab = tabState.createTab({}) + tabState.defaultTabState.keys((key) => { + assert.equal(tabState.defaultTabState.get(key), tab.get(key)) + }) + }) + + it('merges supplied and default values', function () { + let tab = tabState.createTab({tabId: 20, myProp: 'test'}) + tabState.defaultTabState.keys((key) => { + if (key !== 'tabId') { + assert.equal(tabState.defaultTabState.get(key), tab.get(key)) + } + }) + assert.equal(20, tab.get('tabId')) + assert.equal('test', tab.get('myProp')) + }) + }) + + describe('getByTabId', function () { + describe('`tabId` exists in appState', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + windowId: 1, + frameKey: 1, + tabId: 2 + } + ])) + }) + + it('returns the tab for `tabId` from the appState', function () { + let tab = tabState.getByTabId(this.appState, 2) + assert(tab) + assert.equal(1, tab.get('windowId')) + assert.equal(1, tab.get('frameKey')) + assert.equal(2, tab.get('tabId')) + }) + }) + + describe('`tabId` does not exist in appState', function () { + it('returns null', function () { + let tab = tabState.getByTabId(defaultAppState, 2) + assert.equal(null, tab) + }) + }) + }) + + describe('closeTab', function () { + describe('`tabId` exists in appState', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + windowId: 1, + frameKey: 1, + tabId: 1, + myProp: 'test1', + myProp2: 'blah' + }, + { + windowId: 1, + frameKey: 1, + tabId: 2, + myProp: 'test2', + myProp2: 'blah' + } + ])) + + this.newAppState = tabState.closeTab(this.appState, 2) + }) + + it('removes the tab from the appState', function () { + let tab2 = this.newAppState.get('tabs').find((tab) => tab.get('tabId') === 2) + assert.equal(undefined, tab2) + let tab1 = this.newAppState.get('tabs').find((tab) => tab.get('tabId') === 1) + assert(tab1) + }) + + it('does not change other values in the appState', function () { + let tab = this.newAppState.get('tabs').find((tab) => tab.get('tabId') === 1) + assert(tab) + assert.equal('test1', tab.get('myProp')) + assert.equal('blah', tab.get('myProp2')) + assert.equal(1, tab.get('windowId')) + assert.equal(1, tab.get('frameKey')) + assert.equal(1, tab.get('tabId')) + assert.equal(true, this.newAppState.get('otherProp')) + }) + }) + + describe('`tabId` does not exist in appState', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + windowId: 1, + frameKey: 1, + tabId: 1, + myProp: 'test1', + myProp2: 'blah' + } + ])) + + this.newAppState = tabState.closeTab(this.appState, 2) + }) + + it('returns the original appState', function () { + assert(this.appState.equals(this.newAppState)) + }) + }) + }) + + describe('getOrCreateByTabId', function () { + describe('`tabId` exists in appState', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + windowId: 1, + frameKey: 1, + tabId: 2 + } + ])) + }) + + it('returns the tab for `tabId` from the appState', function () { + let tab = tabState.getOrCreateByTabId(this.appState, 2) + assert(tab) + assert.equal(1, tab.get('windowId')) + assert.equal(1, tab.get('frameKey')) + assert.equal(2, tab.get('tabId')) + }) + }) + + describe('`tabId` does not exist in appState', function () { + it('creates a new tab for `tabId`', function () { + let tab = tabState.getOrCreateByTabId(defaultAppState, 2) + assert(tab) + assert.equal(-1, tab.get('windowId')) + assert.equal(-1, tab.get('frameKey')) + assert.equal(2, tab.get('tabId')) + }) + }) + }) + + describe('updateTab', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + windowId: 1, + frameKey: 1, + tabId: 1, + myProp: 'test1', + myProp2: 'blah' + }, + { + windowId: 1, + frameKey: 1, + tabId: 2, + myProp: 'test2', + myProp2: 'blah' + } + ])) + + this.newAppState = tabState.updateTab(this.appState, 2, Immutable.fromJS({ + windowId: 1, + frameKey: 1, + tabId: 2, + myProp: 'test3' + })) + }) + + it('error for no such tabId') + + it('updates the tab values for `tabId` in the appState', function () { + let tab = this.newAppState.get('tabs').find((tab) => tab.get('tabId') === 2) + assert(tab) + assert.equal('test3', tab.get('myProp')) + assert.equal(undefined, tab.get('myProp2')) + assert.equal(1, tab.get('windowId')) + assert.equal(1, tab.get('frameKey')) + assert.equal(2, tab.get('tabId')) + }) + + it('does not change other values in the appState', function () { + let tab = this.newAppState.get('tabs').find((tab) => tab.get('tabId') === 1) + assert(tab) + assert.equal('test1', tab.get('myProp')) + assert.equal('blah', tab.get('myProp2')) + assert.equal(1, tab.get('windowId')) + assert.equal(1, tab.get('frameKey')) + assert.equal(1, tab.get('tabId')) + assert.equal(true, this.newAppState.get('otherProp')) + }) + }) + + describe('getOrCreateByTabId', function () { + describe('`tabId` exists in appState', function () { + before(function () { + this.appState = defaultAppState.set('tabs', Immutable.fromJS([ + { + windowId: 1, + frameKey: 1, + tabId: 2 + } + ])) + }) + + it('returns the tab for `tabId` from the appState', function () { + let tab = tabState.getOrCreateByTabId(this.appState, 2) + assert(tab) + assert.equal(1, tab.get('windowId')) + assert.equal(1, tab.get('frameKey')) + assert.equal(2, tab.get('tabId')) + }) + }) + + describe('`tabId` does not exist in appState', function () { + it('creates a new tab for `tabId`', function () { + let tab = tabState.getOrCreateByTabId(defaultAppState, 2) + assert(tab) + assert.equal(-1, tab.get('windowId')) + assert.equal(-1, tab.get('frameKey')) + assert.equal(2, tab.get('tabId')) + }) + }) + }) + + describe('getPersistentTabState', function () { + before(function () { + this.tab = Immutable.fromJS({ + windowId: 1, + frameKey: 1, + tabId: 2, + loginRequiredDetail: { + request: { url: 'someurl' }, + authInfo: { authInfoProp: 'value' } + } + }) + this.tab = tabState.getPersistentTabState(this.tab) + }) + + it('should keep frameKey', function () { + assert.equal(1, this.tab.get('frameKey')) + }) + + it('should remove windowId', function () { + assert.equal(undefined, this.tab.get('tabId')) + }) + + it('should remove tabId', function () { + assert.equal(undefined, this.tab.get('tabId')) + }) + + it('should remove loginRequiredDetail', function () { + assert.equal(undefined, this.tab.get('loginRequiredDetail')) + }) + }) +})