From 472f152dce2174cdad4a944ea231115e4339ae17 Mon Sep 17 00:00:00 2001 From: Cezar Augusto Date: Thu, 21 Sep 2017 05:05:58 -0300 Subject: [PATCH] Merge pull request #10691 from brave/tabs-observer improve tabs performance and UI with this one simple trick --- .../state/tabContentState/audioState.js | 56 ++ .../state/tabContentState/closeState.js | 52 ++ .../state/tabContentState/faviconState.js | 87 +++ .../state/tabContentState/partitionState.js | 65 +++ .../state/tabContentState/privateState.js | 30 + .../state/tabContentState/titleState.js | 64 ++ app/common/state/tabUIState.js | 160 +++++ app/extensions/brave/img/tabs/close_btn.svg | 1 + .../brave/img/tabs/close_btn_hover.svg | 19 - .../brave/img/tabs/close_btn_normal.svg | 19 - app/extensions/brave/img/tabs/default.svg | 1 + app/renderer/components/styles/animations.js | 14 +- .../components/styles/commonStyles.js | 2 +- app/renderer/components/styles/global.js | 56 +- app/renderer/components/styles/theme.js | 68 +++ .../components/tabs/content/audioTabIcon.js | 72 ++- .../components/tabs/content/closeTabIcon.js | 97 ++-- .../components/tabs/content/favIcon.js | 145 +++-- .../components/tabs/content/newSessionIcon.js | 82 +-- .../components/tabs/content/privateIcon.js | 52 +- .../components/tabs/content/tabIcon.js | 2 +- .../components/tabs/content/tabTitle.js | 73 +-- app/renderer/components/tabs/tab.js | 317 ++++++---- app/renderer/components/tabs/tabs.js | 3 +- app/renderer/lib/observerUtil.js | 42 ++ app/renderer/lib/tabUtil.js | 35 -- app/renderer/reducers/tabContentReducer.js | 30 + docs/state.md | 2 +- js/actions/windowActions.js | 12 +- js/constants/windowConstants.js | 2 +- js/state/frameStateUtil.js | 182 +++--- js/stores/windowStore.js | 12 +- less/main.less | 2 +- less/tabs.less | 12 +- less/window.less | 9 + .../browser/reducers/tabContentReducerTest.js | 63 ++ .../app/common/state/tabContentStateTest.js | 545 ------------------ .../tabContentStateTest/audioStateTest.js | 164 ++++++ .../tabContentStateTest/closeStateTest.js | 165 ++++++ .../tabContentStateTest/faviconStateTest.js | 182 ++++++ .../tabContentStateTest/partitionStateTest.js | 118 ++++ .../tabContentStateTest/privateStateTest.js | 66 +++ .../tabContentStateTest/titleStateTest.js | 157 +++++ test/unit/app/common/state/tabUIStateTest.js | 408 +++++++++++++ .../tabs/content/audioTabIconTest.js | 111 ++-- .../tabs/content/closeTabIconTest.js | 190 +----- .../components/tabs/content/favIconTest.js | 130 +++-- .../tabs/content/newSessionIconTest.js | 269 +++------ .../tabs/content/privateIconTest.js | 233 +++----- .../components/tabs/content/tabTitleTest.js | 237 ++++---- test/unit/js/stores/windowStoreTest.js | 1 - test/unit/state/frameStateUtilTest.js | 33 ++ 52 files changed, 3103 insertions(+), 1846 deletions(-) create mode 100644 app/common/state/tabContentState/audioState.js create mode 100644 app/common/state/tabContentState/closeState.js create mode 100644 app/common/state/tabContentState/faviconState.js create mode 100644 app/common/state/tabContentState/partitionState.js create mode 100644 app/common/state/tabContentState/privateState.js create mode 100644 app/common/state/tabContentState/titleState.js create mode 100644 app/common/state/tabUIState.js create mode 100644 app/extensions/brave/img/tabs/close_btn.svg delete mode 100644 app/extensions/brave/img/tabs/close_btn_hover.svg delete mode 100644 app/extensions/brave/img/tabs/close_btn_normal.svg create mode 100644 app/extensions/brave/img/tabs/default.svg create mode 100644 app/renderer/components/styles/theme.js create mode 100644 app/renderer/lib/observerUtil.js create mode 100644 app/renderer/reducers/tabContentReducer.js create mode 100644 test/unit/app/browser/reducers/tabContentReducerTest.js delete mode 100644 test/unit/app/common/state/tabContentStateTest.js create mode 100644 test/unit/app/common/state/tabContentStateTest/audioStateTest.js create mode 100644 test/unit/app/common/state/tabContentStateTest/closeStateTest.js create mode 100644 test/unit/app/common/state/tabContentStateTest/faviconStateTest.js create mode 100644 test/unit/app/common/state/tabContentStateTest/partitionStateTest.js create mode 100644 test/unit/app/common/state/tabContentStateTest/privateStateTest.js create mode 100644 test/unit/app/common/state/tabContentStateTest/titleStateTest.js create mode 100644 test/unit/app/common/state/tabUIStateTest.js diff --git a/app/common/state/tabContentState/audioState.js b/app/common/state/tabContentState/audioState.js new file mode 100644 index 00000000000..49b8b5835d8 --- /dev/null +++ b/app/common/state/tabContentState/audioState.js @@ -0,0 +1,56 @@ +/* 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/. */ + +// State helpers +const frameStateUtil = require('../../../../js/state/frameStateUtil') + +// Utils +const {isEntryIntersected} = require('../../../../app/renderer/lib/observerUtil') + +module.exports.canPlayAudio = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return frame.get('audioPlaybackActive') || frame.get('audioMuted') +} + +module.exports.isAudioMuted = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + const tabCanPlayAudio = module.exports.canPlayAudio(state, frameKey) + return tabCanPlayAudio && frame.get('audioMuted') +} + +module.exports.showAudioIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return ( + !isEntryIntersected(state, 'tabs') && + module.exports.canPlayAudio(state, frameKey) + ) +} + +module.exports.showAudioTopBorder = (state, frameKey, isPinned) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return ( + module.exports.canPlayAudio(state, frameKey) && + (isEntryIntersected(state, 'tabs') || isPinned) + ) +} diff --git a/app/common/state/tabContentState/closeState.js b/app/common/state/tabContentState/closeState.js new file mode 100644 index 00000000000..4fcc279baea --- /dev/null +++ b/app/common/state/tabContentState/closeState.js @@ -0,0 +1,52 @@ +/* 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/. */ + +// State helpers +const frameStateUtil = require('../../../../js/state/frameStateUtil') + +// Utils +const {isEntryIntersected} = require('../../../../app/renderer/lib/observerUtil') + +// Styles +const {intersection} = require('../../../renderer/components/styles/global') + +module.exports.hasFixedCloseIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return ( + frameStateUtil.isFrameKeyActive(state, frameKey) && + isEntryIntersected(state, 'tabs', intersection.at75) + ) +} + +module.exports.hasRelativeCloseIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return ( + frameStateUtil.getTabHoverState(state, frameKey) && + !isEntryIntersected(state, 'tabs', intersection.at75) + ) +} + +module.exports.showCloseTabIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return !isEntryIntersected(state, 'tabs', intersection.at20) && + ( + module.exports.hasRelativeCloseIcon(state, frameKey) || + module.exports.hasFixedCloseIcon(state, frameKey) + ) +} diff --git a/app/common/state/tabContentState/faviconState.js b/app/common/state/tabContentState/faviconState.js new file mode 100644 index 00000000000..16bac75e219 --- /dev/null +++ b/app/common/state/tabContentState/faviconState.js @@ -0,0 +1,87 @@ +/* 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/. */ + +// Utils +const {isSourceAboutUrl} = require('../../../../js/lib/appUrlUtil') +const frameStateUtil = require('../../../../js/state/frameStateUtil') +const {isEntryIntersected} = require('../../../../app/renderer/lib/observerUtil') + +// Styles +const {intersection} = require('../../../renderer/components/styles/global') + +module.exports.showFavicon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + const isNewTabPage = frameStateUtil.frameLocationMatch(frame, 'about:newtab') + + if (isEntryIntersected(state, 'tabs', intersection.at40)) { + // do not show it at all at minimum ratio (intersection.at12) + if (isEntryIntersected(state, 'tabs', intersection.at12)) { + return false + } + + return ( + // when almost all tab content is covered, + // only show favicon if there's no closeIcon (intersection.at20) + // or otherwise only for the non-active tab + isEntryIntersected(state, 'tabs', intersection.at20) || + !frameStateUtil.isFrameKeyActive(state, frameKey) + ) + } + + // new tab page is the only tab we do not show favicon + return !isNewTabPage +} + +module.exports.getFavicon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + const isLoadingVisible = module.exports.showLoadingIcon(state, frameKey) + + if (frame == null) { + return '' + } + + return !isLoadingVisible && frame.get('icon') +} + +module.exports.showLoadingIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + if (frame.get('loading') == null) { + return false + } + + return ( + !isSourceAboutUrl(frame.get('location')) && + frame.get('loading') + ) +} + +module.exports.showIconWithLessMargin = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return isEntryIntersected(state, 'tabs', intersection.at30) +} + +module.exports.showFaviconAtReducedSize = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return isEntryIntersected(state, 'tabs', intersection.at20) +} diff --git a/app/common/state/tabContentState/partitionState.js b/app/common/state/tabContentState/partitionState.js new file mode 100644 index 00000000000..425bd08d7e6 --- /dev/null +++ b/app/common/state/tabContentState/partitionState.js @@ -0,0 +1,65 @@ +/* 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/. */ + +// State helpers +const tabUIState = require('../tabUIState') +const frameStateUtil = require('../../../../js/state/frameStateUtil') + +// Constants +const {tabs} = require('../../../../js/constants/config') + +module.exports.isPartitionTab = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return !!frame.get('partitionNumber') +} + +module.exports.getPartitionNumber = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return 0 + } + + const partitionNumber = frame.get('partitionNumber') + if (typeof partitionNumber === 'string') { + return partitionNumber.replace(/^partition-/i, '') + } + return partitionNumber +} + +module.exports.getMaxAllowedPartitionNumber = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return 0 + } + + const partitionNumber = module.exports.getPartitionNumber(state, frameKey) + + if (partitionNumber > tabs.maxAllowedNewSessions) { + return tabs.maxAllowedNewSessions + } + return partitionNumber +} + +module.exports.showPartitionIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for showPartitionIcon method') + } + return false + } + + return ( + module.exports.isPartitionTab(state, frameKey) && + tabUIState.showTabEndIcon(state, frameKey) + ) +} diff --git a/app/common/state/tabContentState/privateState.js b/app/common/state/tabContentState/privateState.js new file mode 100644 index 00000000000..727aa632316 --- /dev/null +++ b/app/common/state/tabContentState/privateState.js @@ -0,0 +1,30 @@ +/* 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/. */ + +// State helpers +const tabUIState = require('../tabUIState') +const frameStateUtil = require('../../../../js/state/frameStateUtil') + +module.exports.isPrivateTab = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return !!frame.get('isPrivate') +} + +module.exports.showPrivateIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + return ( + module.exports.isPrivateTab(state, frameKey) && + tabUIState.showTabEndIcon(state, frameKey) + ) +} diff --git a/app/common/state/tabContentState/titleState.js b/app/common/state/tabContentState/titleState.js new file mode 100644 index 00000000000..6bdc5b75f47 --- /dev/null +++ b/app/common/state/tabContentState/titleState.js @@ -0,0 +1,64 @@ +/* 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 locale = require('../../../../js/l10n') + + // State helpers + const partitionState = require('../tabContentState/partitionState') + const privateState = require('../tabContentState/privateState') + const frameStateUtil = require('../../../../js/state/frameStateUtil') + + // Utils + const {isEntryIntersected} = require('../../../../app/renderer/lib/observerUtil') + + // Styles + const {intersection} = require('../../../renderer/components/styles/global') + + module.exports.showTabTitle = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return false + } + + const isNewTabPage = frameStateUtil.frameLocationMatch(frame, 'about:newtab') + const isActive = frameStateUtil.isFrameKeyActive(state, frameKey) + const isPartition = partitionState.isPartitionTab(state, frameKey) + const isPrivate = privateState.isPrivateTab(state, frameKey) + const secondaryIconVisible = !isNewTabPage && (isPartition || isPrivate || isActive) + + // If title is being intersected by ~half with other icons visible + // such as closeTab (activeTab) or session icons, do not show it + if (isEntryIntersected(state, 'tabs', intersection.at45) && secondaryIconVisible) { + return false + } + + // title should never show at such intersection point + return !isEntryIntersected(state, 'tabs', intersection.at40) + } + + module.exports.getDisplayTitle = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + return '' + } + + const isNewTabPage = frameStateUtil.frameLocationMatch(frame, 'about:newtab') + const isAboutBlankPage = frameStateUtil.frameLocationMatch(frame, 'about:blank') + // For renderer initiated navigation, make sure we show Untitled + // until we know what we're loading. We should probably do this for + // all about: pages that we already know the title for so we don't have + // to wait for the title to be parsed. + if (isAboutBlankPage) { + return locale.translation('aboutBlankTitle') + } else if (isNewTabPage) { + return locale.translation('newTab') + } + + // YouTube tries to change the title to add a play icon when + // there is audio. Since we have our own audio indicator we get + // rid of it. + return (frame.get('title') || frame.get('location') || '').replace('▶ ', '') + } diff --git a/app/common/state/tabUIState.js b/app/common/state/tabUIState.js new file mode 100644 index 00000000000..772c014a584 --- /dev/null +++ b/app/common/state/tabUIState.js @@ -0,0 +1,160 @@ +/* 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/. */ + +// Constants +const settings = require('../../../js/constants/settings') + +// State helpers +const partitionState = require('../../common/state/tabContentState/partitionState') +const privateState = require('../../common/state/tabContentState/privateState') +const closeState = require('../../common/state/tabContentState/closeState') +const frameStateUtil = require('../../../js/state/frameStateUtil') + +// Utils +const {isEntryIntersected} = require('../../../app/renderer/lib/observerUtil') +const {getTextColorForBackground} = require('../../../js/lib/color') + +// Settings +const {getSetting} = require('../../../js/settings') + +// Styles +const {intersection} = require('../../renderer/components/styles/global') +const {theme} = require('../../renderer/components/styles/theme') + +module.exports.getThemeColor = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for getThemeColor method') + } + return false + } + + return ( + getSetting(settings.PAINT_TABS) && + (frame.get('themeColor') || frame.get('computedThemeColor')) + ) +} + +module.exports.getTabIconColor = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for getTabIconColor method') + } + return '' + } + + const isPrivate = frame.get('isPrivate') + const isActive = frameStateUtil.isFrameKeyActive(state, frameKey) + const hoverState = frameStateUtil.getTabHoverState(state, frameKey) + const themeColor = frame.get('themeColor') || frame.get('computedThemeColor') + const activeNonPrivateTab = !isPrivate && isActive + const isPrivateTab = isPrivate && (isActive || hoverState) + const defaultColor = isPrivateTab ? 'white' : 'black' + const isPaintTabs = getSetting(settings.PAINT_TABS) + + return activeNonPrivateTab && isPaintTabs && !!themeColor + ? getTextColorForBackground(themeColor) + : defaultColor +} + +module.exports.checkIfTextColor = (state, frameKey, color) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for checkIfTextColor method') + } + return false + } + + return module.exports.getTabIconColor(state, frameKey) === color +} + +module.exports.showTabEndIcon = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for showTabEndIcon method') + } + return false + } + + return ( + !closeState.hasFixedCloseIcon(state, frameKey) && + !closeState.hasRelativeCloseIcon(state, frameKey) && + !isEntryIntersected(state, 'tabs', intersection.at40) + ) +} + +module.exports.addExtraGutterToTitle = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for addExtraGutterToTitle method') + } + return false + } + + return frameStateUtil.frameLocationMatch(frame, 'about:newtab') +} + +module.exports.centralizeTabIcons = (state, frameKey, isPinned) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for centralizeTabIcons method') + } + return false + } + + return isPinned || isEntryIntersected(state, 'tabs', intersection.at40) +} + +module.exports.getTabEndIconBackgroundColor = (state, frameKey) => { + const frame = frameStateUtil.getFrameByKey(state, frameKey) + + if (frame == null) { + if (process.env.NODE_ENV !== 'test') { + console.error('Unable to find frame for getTabEndIconBackgroundColor method') + } + return false + } + + const themeColor = module.exports.getThemeColor(state, frameKey) + const isPrivate = privateState.isPrivateTab(state, frameKey) + const isPartition = partitionState.isPartitionTab(state, frameKey) + const isHover = frameStateUtil.getTabHoverState(state, frameKey) + const isActive = frameStateUtil.isFrameKeyActive(state, frameKey) + const hasCloseIcon = closeState.showCloseTabIcon(state, frameKey) + const isIntersecting = isEntryIntersected(state, 'tabs', intersection.at40) + + let backgroundColor = theme.tab.background + + if (isActive && themeColor) { + backgroundColor = themeColor + } + if (isActive && !themeColor) { + backgroundColor = theme.tab.active.background + } + if (isIntersecting) { + backgroundColor = 'transparent' + } + if (!isActive && isPrivate) { + backgroundColor = theme.tab.private.background + } + if ((isActive || isHover) && isPrivate) { + backgroundColor = theme.tab.active.private.background + } + + return isPartition || isPrivate || hasCloseIcon + ? `linear-gradient(to left, ${backgroundColor} 10px, transparent 40px)` + : `linear-gradient(to left, ${backgroundColor} 0, transparent 12px)` +} diff --git a/app/extensions/brave/img/tabs/close_btn.svg b/app/extensions/brave/img/tabs/close_btn.svg new file mode 100644 index 00000000000..926636ea455 --- /dev/null +++ b/app/extensions/brave/img/tabs/close_btn.svg @@ -0,0 +1 @@ + diff --git a/app/extensions/brave/img/tabs/close_btn_hover.svg b/app/extensions/brave/img/tabs/close_btn_hover.svg deleted file mode 100644 index 20d2664d403..00000000000 --- a/app/extensions/brave/img/tabs/close_btn_hover.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - -close_btn_hover - - - - - - - - - - diff --git a/app/extensions/brave/img/tabs/close_btn_normal.svg b/app/extensions/brave/img/tabs/close_btn_normal.svg deleted file mode 100644 index 74a67605ec3..00000000000 --- a/app/extensions/brave/img/tabs/close_btn_normal.svg +++ /dev/null @@ -1,19 +0,0 @@ - - - - -close_btn_normal - - - - - - - - - - diff --git a/app/extensions/brave/img/tabs/default.svg b/app/extensions/brave/img/tabs/default.svg new file mode 100644 index 00000000000..87b2ba2db9b --- /dev/null +++ b/app/extensions/brave/img/tabs/default.svg @@ -0,0 +1 @@ + diff --git a/app/renderer/components/styles/animations.js b/app/renderer/components/styles/animations.js index 87269b944f9..54c421d03aa 100644 --- a/app/renderer/components/styles/animations.js +++ b/app/renderer/components/styles/animations.js @@ -20,7 +20,19 @@ const opacityIncreaseKeyframes = { } } +// TODO: this could be a function with param included +// to which property should be changed +const widthIncreaseKeyframes = (start, end) => ({ + 'from': { + width: start + }, + 'to': { + width: end + } +}) + module.exports = { spinKeyframes, - opacityIncreaseKeyframes + opacityIncreaseKeyframes, + widthIncreaseKeyframes } diff --git a/app/renderer/components/styles/commonStyles.js b/app/renderer/components/styles/commonStyles.js index 0ffa4b810fa..35bfef0d1de 100644 --- a/app/renderer/components/styles/commonStyles.js +++ b/app/renderer/components/styles/commonStyles.js @@ -160,7 +160,7 @@ const styles = StyleSheet.create({ notificationBar__notificationItem: { backgroundColor: globalStyles.color.notificationItemColor, boxSizing: 'border-box', - borderTop: `1px solid ${globalStyles.color.tabsToolbarBorderColor}`, + boxShadow: `0 -1px 0 ${globalStyles.color.tabsToolbarBorderColor}`, lineHeight: '24px', padding: '8px 20px' }, diff --git a/app/renderer/components/styles/global.js b/app/renderer/components/styles/global.js index d6341161e71..0ec4533723a 100644 --- a/app/renderer/components/styles/global.js +++ b/app/renderer/components/styles/global.js @@ -1,26 +1,18 @@ /* 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/. */ +* 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 {opacityIncreaseKeyframes} = require('./animations') /** - * Historically this file includes styles with no defined criteria. - * Imagine this file as a future reference for theming, in a way - * that each component should be an object wrapping all properties - * that would change in a dark mode, for example. - * - * Valid as well for things that needs to be fully global, - * i.e. breakpoints, icons and zIndexes. - * - * Thus said, please take preference for inlined styles in the component itself. - * If you really feel repetitve writing the same style for a given component, - * consider including a variable inside component. - * - * TODO: - * remove unnecessary styles properties (as items get refactored) - * Remove fully global items and take preference for component properties (@see button) - */ +* Use this file when the style you need +* is applied in more than one element, or depends on it +* Use theme.js file to include colors that can be customized +* +* TODO: +* remove unnecessary styles properties (as items get refactored) +* migrate customizable options to theme.js +*/ const globalStyles = { defaultFontFamily: `-apple-system, BlinkMacSystemFont, "Segoe UI"` + @@ -33,17 +25,18 @@ const globalStyles = { breakpointExtensionButtonPadding: '720px', breakpointSmallWin32: '650px', breakpointTinyWin32: '500px', - breakpointNewPrivateTab: '890px', - tab: { - default: '184px', // match tabArea max-width - large: '120px', - largeMedium: '83px', - medium: '66px', - mediumSmall: '53px', - small: '46px', - extraSmall: '40px', - smallest: '19px' - } + breakpointNewPrivateTab: '890px' // page's breakpoint for the private tab page + }, + intersection: { + // whereas 1 === 100% + noIntersection: 1, + at75: 0.75, + at60: 0.6, + at45: 0.45, + at40: 0.4, + at30: 0.3, + at20: 0.20, + at12: 0.125 }, color: { commonTextColor: '#3b3b3b', @@ -126,6 +119,7 @@ const globalStyles = { carotRadius: '8px' }, spacing: { + sentinelSize: '120px', navigatorHeight: '48px', defaultSpacing: '12px', defaultFontSize: '13px', @@ -168,9 +162,10 @@ const globalStyles = { aboutPageDetailsPageWidth: '704px', aboutPageSectionPadding: '24px', aboutPageSectionMargin: '10px', - defaultTabPadding: '0 4px', + defaultTabMargin: '6px', defaultIconPadding: '2px', iconSize: '16px', + sessionIconSize: '15px', closeIconSize: '13px', narrowIconSize: '12px', dialogWidth: '422px', @@ -219,6 +214,7 @@ const globalStyles = { zindexWindowIsPreview: '1100', zindexDownloadsBar: '1000', zindexTabs: '1000', + zindexTabsAudioTopBorder: '1001', zindexTabsThumbnail: '1100', zindexTabsDragIndicator: '1100', zindexNavigationBar: '2000', diff --git a/app/renderer/components/styles/theme.js b/app/renderer/components/styles/theme.js new file mode 100644 index 00000000000..00a5b00088e --- /dev/null +++ b/app/renderer/components/styles/theme.js @@ -0,0 +1,68 @@ +/* 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/. */ + + /** + * Includes color options for theming + * This should be used as a boilerplate for all + * future theming, including darkUI. + * Note: If an element is not color-related, it should go into global.js + */ + module.exports.theme = { + tab: { + // mimics chrome hover effect + transition: ` + background-color 200ms cubic-bezier(0.26, 0.63, 0.39, 0.65), + color 200ms cubic-bezier(0.26, 0.63, 0.39, 0.65) + `, + background: '#ddd', + borderColor: '#bbb', + color: '#5a5a5a', + + hover: { + background: 'rgba(255, 255, 255, 0.4)' + }, + + forWindows: { + color: '#555' + }, + + active: { + background: 'rgba(255, 255, 255, 0.8)', + + private: { + background: '#4b3c6e', + color: '#fff' + } + }, + + private: { + background: '#d9d6e0', + color: '#4b3c6e' + }, + + content: { + icon: { + default: { + primary: '#fff', + secondary: 'rgb(101, 101, 101)' + }, + + private: { + background: { + active: '#fff', + notActive: '#000' + } + }, + + audio: { + color: '#69B9F9' + }, + + close: { + filter: 'invert(100%) grayscale(1) contrast(0.5) brightness(160%)' + } + } + } + } + } diff --git a/app/renderer/components/tabs/content/audioTabIcon.js b/app/renderer/components/tabs/content/audioTabIcon.js index f286d6dc53d..68f891c1adf 100644 --- a/app/renderer/components/tabs/content/audioTabIcon.js +++ b/app/renderer/components/tabs/content/audioTabIcon.js @@ -4,24 +4,22 @@ const React = require('react') const {StyleSheet, css} = require('aphrodite/no-important') -const Immutable = require('immutable') // Components const ReduxComponent = require('../../reduxComponent') const TabIcon = require('./tabIcon') -// State -const tabState = require('../../../../common/state/tabState') +// State helpers +const audioState = require('../../../../common/state/tabContentState/audioState') +const frameStateUtil = require('../../../../../js/state/frameStateUtil') // Actions const windowActions = require('../../../../../js/actions/windowActions') -// Utils -const frameStateUtil = require('../../../../../js/state/frameStateUtil') - // Styles +const {widthIncreaseKeyframes} = require('../../styles/animations') const globalStyles = require('../../styles/global') -const tabStyles = require('../../styles/tab') +const {theme} = require('../../styles/theme') class AudioTabIcon extends React.Component { constructor (props) { @@ -30,48 +28,70 @@ class AudioTabIcon extends React.Component { } get audioIcon () { - const isNotMuted = this.props.pageCanPlayAudio && !this.props.audioMuted - - return isNotMuted + return this.props.audioPlaying ? globalStyles.appIcons.volumeOn : globalStyles.appIcons.volumeOff } toggleMute (event) { event.stopPropagation() - windowActions.setAudioMuted(this.props.frameKey, this.props.tabId, !this.props.audioMuted) + windowActions + .setAudioMuted(this.props.frameKey, this.props.tabId, this.props.audioPlaying) } mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') - - // AudioIcon will never be created if there is no frameKey, but for consistency - // across other components I added teh || Immutable.Map() - const frame = frameStateUtil.getFrameByKey(currentWindow, ownProps.frameKey) || Immutable.Map() + const tabId = ownProps.tabId + const frameKey = frameStateUtil.getFrameKeyByTabId(currentWindow, tabId) const props = {} - // used in other functions - props.frameKey = ownProps.frameKey - props.pageCanPlayAudio = !!frame.get('audioPlaybackActive') - props.tabId = frame.get('tabId', tabState.TAB_ID_NONE) - props.audioMuted = frame.get('audioMuted') + props.frameKey = frameKey + props.showAudioIcon = audioState.showAudioIcon(currentWindow, frameKey) + props.audioPlaying = !audioState.isAudioMuted(currentWindow, frameKey) + props.canPlayAudio = audioState.canPlayAudio(currentWindow, frameKey) + props.isPinned = frameStateUtil.isPinned(currentWindow, frameKey) + props.tabId = tabId return props } render () { + if (this.props.isPinned || !this.props.showAudioIcon) { + return null + } + return } } -module.exports = ReduxComponent.connect(AudioTabIcon) - const styles = StyleSheet.create({ - audioIcon: { - color: globalStyles.color.highlightBlue, - fontSize: '16px' + audioTab__icon: { + width: 0, + animationName: widthIncreaseKeyframes(0, globalStyles.spacing.iconSize), + animationDelay: '50ms', + animationTimingFunction: 'linear', + animationDuration: '100ms', + animationFillMode: 'forwards', + + overflow: 'hidden', + margin: '0 -2px 0 2px', + zIndex: globalStyles.zindex.zindexTabsAudioTopBorder, + color: theme.tab.content.icon.audio.color, + fontSize: '13px', + height: globalStyles.spacing.iconSize, + backgroundSize: globalStyles.spacing.iconSize, + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + display: 'flex', + alignSelf: 'center', + position: 'relative', + textAlign: 'center', + justifyContent: 'center' } }) + +module.exports = ReduxComponent.connect(AudioTabIcon) diff --git a/app/renderer/components/tabs/content/closeTabIcon.js b/app/renderer/components/tabs/content/closeTabIcon.js index 577350c99cc..43f0973e31a 100644 --- a/app/renderer/components/tabs/content/closeTabIcon.js +++ b/app/renderer/components/tabs/content/closeTabIcon.js @@ -1,30 +1,28 @@ /* 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/. */ +* 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 React = require('react') const {StyleSheet, css} = require('aphrodite/no-important') -const Immutable = require('immutable') // Components const ReduxComponent = require('../../reduxComponent') const TabIcon = require('./tabIcon') -// State -const tabContentState = require('../../../../common/state/tabContentState') -const tabState = require('../../../../common/state/tabState') +// State helpers +const tabUIState = require('../../../../common/state/tabUIState') +const closeState = require('../../../../common/state/tabContentState/closeState') +const frameStateUtil = require('../../../../../js/state/frameStateUtil') // Actions const windowActions = require('../../../../../js/actions/windowActions') const appActions = require('../../../../../js/actions/appActions') -// Utils -const frameStateUtil = require('../../../../../js/state/frameStateUtil') - // Styles -const globalStyles = require('../../styles/global') -const closeTabHoverSvg = require('../../../../extensions/brave/img/tabs/close_btn_hover.svg') -const closeTabSvg = require('../../../../extensions/brave/img/tabs/close_btn_normal.svg') +const {theme} = require('../../styles/theme') +const {spacing, zindex} = require('../../styles/global') +const {opacityIncreaseKeyframes} = require('../../styles/animations') +const closeTabSvg = require('../../../../extensions/brave/img/tabs/close_btn.svg') class CloseTabIcon extends React.Component { constructor (props) { @@ -49,32 +47,33 @@ class CloseTabIcon extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') - const frameKey = ownProps.frameKey - const isPinnedTab = frameStateUtil.isPinned(currentWindow, frameKey) - const frame = frameStateUtil.getFrameByKey(currentWindow, frameKey) || Immutable.Map() + const tabId = ownProps.tabId + const frameKey = frameStateUtil.getFrameKeyByTabId(currentWindow, tabId) + const isPinned = frameStateUtil.isPinned(currentWindow, frameKey) const props = {} - // used in renderer - props.showCloseIcon = !isPinnedTab && - ( - tabContentState.hasRelativeCloseIcon(currentWindow, frameKey) || - tabContentState.hasFixedCloseIcon(currentWindow, frameKey) - ) - - // used in functions - props.frameKey = frameKey + props.isPinned = isPinned props.fixTabWidth = ownProps.fixTabWidth - props.tabId = frame.get('tabId', tabState.TAB_ID_NONE) - props.hasFrame = !frame.isEmpty() + props.hasFrame = frameStateUtil.hasFrame(currentWindow, frameKey) + props.centralizeTabIcons = tabUIState.centralizeTabIcons(currentWindow, frameKey, isPinned) + props.showCloseIcon = closeState.showCloseTabIcon(currentWindow, frameKey) + props.tabId = tabId return props } render () { + if (this.props.isPinned || !this.props.showCloseIcon) { + return null + } + return } } @@ -79,20 +97,53 @@ class Favicon extends React.Component { module.exports = ReduxComponent.connect(Favicon) const styles = StyleSheet.create({ - faviconNarrowView: { - minWidth: 'auto', - width: globalStyles.spacing.narrowIconSize, - backgroundSize: 'contain', - padding: 0, - fontSize: '10px', - backgroundPosition: 'center center' + icon: { + opacity: 0, + willChange: 'opacity', + animationName: opacityIncreaseKeyframes, + animationDelay: '50ms', + animationTimingFunction: 'linear', + animationDuration: '200ms', + animationFillMode: 'forwards', + + position: 'relative', + boxSizing: 'border-box', + width: spacing.iconSize, + height: spacing.iconSize, + backgroundSize: spacing.iconSize, + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + display: 'flex', + alignSelf: 'center' + }, + + icon_lessMargin: { + margin: 0 }, - loadingIcon: { + icon_reducedSize: { + width: spacing.narrowIconSize, + height: '-webkit-fill-available', + alignItems: 'center', + backgroundSize: spacing.narrowIconSize + }, + + icon__loading: { + position: 'absolute', + left: 0, + willChange: 'transform', backgroundImage: `url(${loadingIconSvg})`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'top left', animationName: spinKeyframes, animationTimingFunction: 'linear', animationDuration: '1200ms', animationIterationCount: 'infinite' + }, + + icon__default: { + WebkitMaskRepeat: 'no-repeat', + WebkitMaskPosition: 'center', + WebkitMaskImage: `url(${defaultIconSvg})` } }) diff --git a/app/renderer/components/tabs/content/newSessionIcon.js b/app/renderer/components/tabs/content/newSessionIcon.js index 443da11024a..47533c08552 100644 --- a/app/renderer/components/tabs/content/newSessionIcon.js +++ b/app/renderer/components/tabs/content/newSessionIcon.js @@ -1,65 +1,63 @@ /* 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/. */ +* 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 React = require('react') const {StyleSheet, css} = require('aphrodite/no-important') -const Immutable = require('immutable') // Components const ReduxComponent = require('../../reduxComponent') const TabIcon = require('./tabIcon') // State -const tabContentState = require('../../../../common/state/tabContentState') - -// Constants -const {tabs} = require('../../../../../js/constants/config') - -// Utils +const partitionState = require('../../../../common/state/tabContentState/partitionState') +const tabUIState = require('../../../../common/state/tabUIState') const frameStateUtil = require('../../../../../js/state/frameStateUtil') // Styles -const tabStyles = require('../../styles/tab') +const globalStyles = require('../../styles/global') +const {opacityIncreaseKeyframes} = require('../../styles/animations') const newSessionSvg = require('../../../../extensions/brave/img/tabs/new_session.svg') class NewSessionIcon extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') - const frameKey = ownProps.frameKey - const frame = frameStateUtil.getFrameByKey(currentWindow, frameKey) || Immutable.Map() - const partition = frame.get('partitionNumber') + const tabId = ownProps.tabId + const frameKey = frameStateUtil.getFrameKeyByTabId(currentWindow, tabId) const props = {} - // used in renderer + props.isPinned = frameStateUtil.isPinned(currentWindow, frameKey) + props.showPartitionIcon = partitionState.showPartitionIcon(currentWindow, frameKey) props.isActive = frameStateUtil.isFrameKeyActive(currentWindow, frameKey) - props.iconColor = tabContentState.getTabIconColor(currentWindow, frameKey) - props.partitionNumber = typeof partition === 'string' - ? partition.replace(/^partition-/i, '') - : partition - props.partitionIndicator = props.partitionNumber > tabs.maxAllowedNewSessions - ? tabs.maxAllowedNewSessions - : props.partitionNumber - - // used in functions - props.frameKey = frameKey + props.textIsWhite = tabUIState.checkIfTextColor(currentWindow, frameKey, 'white') + props.partitionNumber = partitionState.getMaxAllowedPartitionNumber(currentWindow, frameKey) + props.tabId = tabId return props } render () { - const newSession = StyleSheet.create({ - indicator: { - // Based on getTextColorForBackground() icons can be only black or white. - filter: this.props.isActive && this.props.iconColor === 'white' ? 'invert(100%)' : 'none' + if ( + this.props.isPinned || + !this.props.showPartitionIcon || + this.props.partitionNumber === 0 + ) { + return null + } + + const newSessionProps = StyleSheet.create({ + newSession__indicator: { + filter: this.props.isActive && this.props.textIsWhite + ? 'invert(100%)' + : 'none' } }) return } @@ -68,9 +66,25 @@ class NewSessionIcon extends React.Component { module.exports = ReduxComponent.connect(NewSessionIcon) const styles = StyleSheet.create({ - newSession: { - position: 'relative', + newSession__icon: { + opacity: 0, + willChange: 'opacity', + animationName: opacityIncreaseKeyframes, + animationDelay: '100ms', + animationTimingFunction: 'linear', + animationDuration: '200ms', + animationFillMode: 'forwards', + + zIndex: globalStyles.zindex.zindexTabsThumbnail, + boxSizing: 'border-box', + display: 'flex', + alignItems: 'center', backgroundImage: `url(${newSessionSvg})`, - backgroundPosition: 'left' + backgroundPosition: 'center left', + backgroundRepeat: 'no-repeat', + backgroundSize: '13px', + width: globalStyles.spacing.iconSize, + height: globalStyles.spacing.iconSize, + marginRight: globalStyles.spacing.defaultTabMargin } }) diff --git a/app/renderer/components/tabs/content/privateIcon.js b/app/renderer/components/tabs/content/privateIcon.js index 2f3f3c3225e..92838736b18 100644 --- a/app/renderer/components/tabs/content/privateIcon.js +++ b/app/renderer/components/tabs/content/privateIcon.js @@ -1,6 +1,6 @@ /* 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/. */ +* 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 React = require('react') const {StyleSheet, css} = require('aphrodite/no-important') @@ -9,39 +9,47 @@ const {StyleSheet, css} = require('aphrodite/no-important') const ReduxComponent = require('../../reduxComponent') const TabIcon = require('./tabIcon') -// Utils +// State helpers +const privateState = require('../../../../common/state/tabContentState/privateState') const frameStateUtil = require('../../../../../js/state/frameStateUtil') // Styles +const {theme} = require('../../styles/theme') const globalStyles = require('../../styles/global') -const tabStyles = require('../../styles/tab') +const {opacityIncreaseKeyframes} = require('../../styles/animations') const privateSvg = require('../../../../extensions/brave/img/tabs/private.svg') class PrivateIcon extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') - const frameKey = ownProps.frameKey + const tabId = ownProps.tabId + const frameKey = frameStateUtil.getFrameKeyByTabId(currentWindow, tabId) const props = {} - // used in renderer + props.isPinned = frameStateUtil.isPinned(currentWindow, frameKey) props.isActive = frameStateUtil.isFrameKeyActive(currentWindow, frameKey) - - // used in functions - props.frameKey = frameKey + props.showPrivateIcon = privateState.showPrivateIcon(currentWindow, frameKey) + props.tabId = tabId return props } render () { - const privateStyles = StyleSheet.create({ - icon: { - backgroundColor: this.props.isActive ? globalStyles.color.white100 : globalStyles.color.black100 + if (this.props.isPinned || !this.props.showPrivateIcon) { + return null + } + + const privateProps = StyleSheet.create({ + private__icon_color: { + backgroundColor: this.props.isActive + ? theme.tab.content.icon.private.background.active + : theme.tab.content.icon.private.background.notActive } }) return } } @@ -49,9 +57,23 @@ class PrivateIcon extends React.Component { module.exports = ReduxComponent.connect(PrivateIcon) const styles = StyleSheet.create({ - secondaryIcon: { + private__icon: { + opacity: 0, + willChange: 'opacity', + animationName: opacityIncreaseKeyframes, + animationDelay: '100ms', + animationTimingFunction: 'linear', + animationDuration: '200ms', + animationFillMode: 'forwards', + + zIndex: globalStyles.zindex.zindexTabsThumbnail, + boxSizing: 'border-box', WebkitMaskRepeat: 'no-repeat', WebkitMaskPosition: 'center', - WebkitMaskImage: `url(${privateSvg})` + WebkitMaskImage: `url(${privateSvg})`, + WebkitMaskSize: globalStyles.spacing.sessionIconSize, + width: globalStyles.spacing.sessionIconSize, + height: globalStyles.spacing.sessionIconSize, + marginRight: globalStyles.spacing.defaultTabMargin } }) diff --git a/app/renderer/components/tabs/content/tabIcon.js b/app/renderer/components/tabs/content/tabIcon.js index 8f8c32fabde..b98851ae97a 100644 --- a/app/renderer/components/tabs/content/tabIcon.js +++ b/app/renderer/components/tabs/content/tabIcon.js @@ -24,7 +24,7 @@ class TabIcon extends ImmutableComponent { width: globalStyles.spacing.iconSize, height: globalStyles.spacing.iconSize, alignItems: 'center', - justifyContent: this.props.symbolContent ? 'flex-end' : 'left', + justifyContent: this.props.symbolContent ? 'flex-end' : 'center', fontWeight: this.props.symbolContent ? 'bold' : 'normal', color: this.props.symbolContent ? globalStyles.color.black100 : 'inherit' } diff --git a/app/renderer/components/tabs/content/tabTitle.js b/app/renderer/components/tabs/content/tabTitle.js index b57e414512a..4c6be4a3c18 100644 --- a/app/renderer/components/tabs/content/tabTitle.js +++ b/app/renderer/components/tabs/content/tabTitle.js @@ -1,6 +1,6 @@ /* 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/. */ +* 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 React = require('react') const {StyleSheet, css} = require('aphrodite/no-important') @@ -8,8 +8,10 @@ const {StyleSheet, css} = require('aphrodite/no-important') // Components const ReduxComponent = require('../../reduxComponent') -// State -const tabContentState = require('../../../../common/state/tabContentState') +// State helpers +const titleState = require('../../../../common/state/tabContentState/titleState') +const frameStateUtil = require('../../../../../js/state/frameStateUtil') +const tabUIState = require('../../../../common/state/tabUIState') // Utils const platformUtil = require('../../../../common/lib/platformUtil') @@ -22,36 +24,34 @@ const globalStyles = require('../../styles/global') class TabTitle extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') - const frameKey = ownProps.frameKey - const tabIconColor = tabContentState.getTabIconColor(currentWindow, frameKey) + const tabId = ownProps.tabId + const frameKey = frameStateUtil.getFrameKeyByTabId(currentWindow, tabId) const props = {} - // used in renderer - props.enforceFontVisibility = isDarwin && tabIconColor === 'white' - props.tabIconColor = tabIconColor - props.displayTitle = tabContentState.getDisplayTitle(currentWindow, frameKey) - - // used in functions - props.frameKey = frameKey + props.isWindows = isWindows + props.isDarwin = isDarwin + props.isPinned = frameStateUtil.isPinned(currentWindow, frameKey) + props.showTabTitle = titleState.showTabTitle(currentWindow, frameKey) + props.displayTitle = titleState.getDisplayTitle(currentWindow, frameKey) + props.addExtraGutter = tabUIState.addExtraGutterToTitle(currentWindow, frameKey) + props.isTextWhite = tabUIState.checkIfTextColor(currentWindow, frameKey, 'white') + props.tabId = tabId return props } render () { - const titleStyles = StyleSheet.create({ - gradientText: { - backgroundImage: `-webkit-linear-gradient(left, - ${this.props.tabIconColor} 90%, ${globalStyles.color.almostInvisible} 100%)` - } - }) + if (this.props.isPinned || !this.props.showTabTitle) { + return null + } return
{this.props.displayTitle}
@@ -61,28 +61,29 @@ class TabTitle extends React.Component { module.exports = ReduxComponent.connect(TabTitle) const styles = StyleSheet.create({ - tabTitle: { + + tab__title: { + boxSizing: 'border-box', display: 'flex', - flex: '1', + flex: 1, userSelect: 'none', - boxSizing: 'border-box', fontSize: globalStyles.fontSize.tabTitle, - overflow: 'hidden', - whiteSpace: 'nowrap', lineHeight: '1.6', - padding: globalStyles.spacing.defaultTabPadding, - color: 'transparent', - WebkitBackgroundClip: 'text', - // prevents the title from being the target of mouse events. - pointerEvents: 'none' + minWidth: 0, // see https://stackoverflow.com/a/36247448/4902448 + marginLeft: '4px', + overflow: 'hidden' }, - enforceFontVisibility: { - fontWeight: '600' + tab__title_isDarwin: { + fontWeight: '400' }, - tabTitleForWindows: { + tab__title_isWindows: { fontWeight: '500', fontSize: globalStyles.fontSize.tabTitle + }, + + tab__title_extraGutter: { + margin: '0 2px' } }) diff --git a/app/renderer/components/tabs/tab.js b/app/renderer/components/tabs/tab.js index 7be5585f9d6..2ff194c2bdc 100644 --- a/app/renderer/components/tabs/tab.js +++ b/app/renderer/components/tabs/tab.js @@ -1,9 +1,9 @@ /* 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/. */ +* 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 React = require('react') -const {StyleSheet, css} = require('aphrodite') +const {StyleSheet, css} = require('aphrodite/no-important') const Immutable = require('immutable') // Components @@ -23,15 +23,18 @@ const windowActions = require('../../../../js/actions/windowActions') // Store const windowStore = require('../../../../js/stores/windowStore') -// State -const tabContentState = require('../../../common/state/tabContentState') +// State helpers +const privateState = require('../../../common/state/tabContentState/privateState') +const audioState = require('../../../common/state/tabContentState/audioState') +const tabUIState = require('../../../common/state/tabUIState') const tabState = require('../../../common/state/tabState') // Constants const dragTypes = require('../../../../js/constants/dragTypes') // Styles -const styles = require('../styles/tab') +const globalStyles = require('../styles/global') +const {theme} = require('../styles/theme') // Utils const cx = require('../../../../js/lib/classSet') @@ -39,16 +42,11 @@ const {getTextColorForBackground} = require('../../../../js/lib/color') const {isIntermediateAboutPage} = require('../../../../js/lib/appUrlUtil') const contextMenus = require('../../../../js/contextMenus') const dnd = require('../../../../js/dnd') -const throttle = require('../../../../js/lib/throttle') const frameStateUtil = require('../../../../js/state/frameStateUtil') -const { - getTabBreakpoint, - tabUpdateFrameRate, - hasBreakpoint, - hasTabAsRelatedTarget -} = require('../../lib/tabUtil') +const {hasTabAsRelatedTarget} = require('../../lib/tabUtil') const isWindows = require('../../../common/lib/platformUtil').isWindows() const {getCurrentWindowId} = require('../../currentWindow') +const {setObserver} = require('../../lib/observerUtil') const UrlUtil = require('../../../../js/lib/urlutil') class Tab extends React.Component { @@ -57,11 +55,12 @@ class Tab extends React.Component { this.onMouseMove = this.onMouseMove.bind(this) this.onMouseEnter = this.onMouseEnter.bind(this) this.onMouseLeave = this.onMouseLeave.bind(this) - this.onUpdateTabSize = this.onUpdateTabSize.bind(this) + this.onDrag = this.onDrag.bind(this) this.onDragStart = this.onDragStart.bind(this) this.onDragEnd = this.onDragEnd.bind(this) this.onDragOver = this.onDragOver.bind(this) this.onClickTab = this.onClickTab.bind(this) + this.onObserve = this.onObserve.bind(this) this.tabNode = null } @@ -123,7 +122,19 @@ class Tab extends React.Component { } onDragStart (e) { + // showing up the sentinel while dragging leads to show the shadow + // of the next tab. See 10691#issuecomment-329854096 + // this is added back to original size when onDrag event is happening + this.tabSentinel.style.width = 0 + dnd.onDragStart(dragTypes.TAB, this.frame, e) + // cancel tab preview while dragging. see #10103 + windowActions.setTabHoverState(this.props.frameKey, false, false) + } + + onDrag () { + // re-enable the tabSentinel while dragging + this.tabSentinel.style.width = globalStyles.spacing.sentinelSize } onDragEnd (e) { @@ -189,34 +200,34 @@ class Tab extends React.Component { } } - get tabSize () { - const tab = this.tabNode - // Avoid TypeError keeping it null until component is mounted - return tab && !this.props.isPinnedTab ? tab.getBoundingClientRect().width : null - } - - onUpdateTabSize () { - const currentSize = getTabBreakpoint(this.tabSize) - // Avoid updating breakpoint when user enters fullscreen (see #7301) - // Also there can be a race condition for pinned tabs if we update when not needed - // since a new tab component with the same key gets created which is not pinned. - if (this.props.breakpoint !== currentSize && !this.props.hasTabInFullScreen) { - windowActions.setTabBreakpoint(this.props.frameKey, currentSize) + componentDidMount () { + // unobserve tabs that we don't need. This will + // likely be made by onObserve method but added again as + // just to double-check + if (this.props.isPinned) { + this.observer && this.observer.unobserve(this.tabSentinel) } - } + const threshold = Object.values(globalStyles.intersection) + // At this moment Chrome can't handle unitless zeroes for rootMargin + // see https://github.com/w3c/IntersectionObserver/issues/244 + const margin = '0px' + this.observer = setObserver(this.tabSentinel, threshold, margin, this.onObserve) + this.observer.observe(this.tabSentinel) - componentDidMount () { - this.onUpdateTabSize() this.tabNode.addEventListener('auxclick', this.onAuxClick.bind(this)) - window.addEventListener('resize', throttle(this.onUpdateTabSize, tabUpdateFrameRate), { passive: true }) } - componentDidUpdate () { - this.onUpdateTabSize() + componentWillUnmount () { + this.observer.unobserve(this.tabSentinel) } - componentWillUnmount () { - window.removeEventListener('resize', this.onUpdateTabSize) + onObserve (entries) { + if (this.props.isPinnedTab) { + return + } + // we only have one entry + const entry = entries[0] + windowActions.setTabIntersectionState(this.props.frameKey, entry.intersectionRatio) } get fixTabWidth () { @@ -231,47 +242,35 @@ class Tab extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') const frame = frameStateUtil.getFrameByKey(currentWindow, ownProps.frameKey) || Immutable.Map() + const frameKey = ownProps.frameKey + const tabId = frame.get('tabId', tabState.TAB_ID_NONE) + const isPinned = frameStateUtil.isPinned(currentWindow, frameKey) + const partOfFullPageSet = ownProps.partOfFullPageSet + + // TODO: this should have its own method const notifications = state.get('notifications') const notificationOrigins = notifications ? notifications.map(bar => bar.get('frameOrigin')) : false const notificationBarActive = frame.get('location') && notificationOrigins && notificationOrigins.includes(UrlUtil.getUrlOrigin(frame.get('location'))) - const hasSeconardImage = tabContentState.hasVisibleSecondaryIcon(currentWindow, ownProps.frameKey) - const breakpoint = frame.get('breakpoint') - const partition = typeof frame.get('partitionNumber') === 'string' - ? frame.get('partitionNumber').replace(/^partition-/i, '') - : frame.get('partitionNumber') const props = {} - // used in renderer - props.frameKey = ownProps.frameKey - props.isPrivateTab = frame.get('isPrivate') - props.breakpoint = frame.get('breakpoint') + // TODO: this should have its own method props.notificationBarActive = notificationBarActive - props.isActive = frameStateUtil.isFrameKeyActive(currentWindow, props.frameKey) + props.frameKey = frameKey + props.isPinnedTab = isPinned + props.isPrivateTab = privateState.isPrivateTab(currentWindow, frameKey) + props.isActive = frameStateUtil.isFrameKeyActive(currentWindow, frameKey) props.tabWidth = currentWindow.getIn(['ui', 'tabs', 'fixTabWidth']) - props.isPinnedTab = frameStateUtil.isPinned(currentWindow, props.frameKey) - props.canPlayAudio = tabContentState.canPlayAudio(currentWindow, props.frameKey) - props.themeColor = tabContentState.getThemeColor(currentWindow, props.frameKey) - props.isNarrowView = tabContentState.isNarrowView(currentWindow, props.frameKey) - props.isNarrowestView = tabContentState.isNarrowestView(currentWindow, props.frameKey) - props.isPlayIndicatorBreakpoint = tabContentState.isMediumView(currentWindow, props.frameKey) || props.isNarrowView + props.themeColor = tabUIState.getThemeColor(currentWindow, frameKey) props.title = frame.get('title') - props.showSessionIcon = partition && hasSeconardImage - props.showPrivateIcon = props.isPrivateTab && hasSeconardImage - props.showFavIcon = !((hasBreakpoint(breakpoint, 'extraSmall') && props.isActive) || frame.get('location') === 'about:newtab') - props.showAudioIcon = breakpoint === 'default' && !!frame.get('audioPlaybackActive') - props.partOfFullPageSet = ownProps.partOfFullPageSet - props.showTitle = !props.isPinnedTab && - !( - (hasBreakpoint(breakpoint, ['mediumSmall', 'small']) && props.isActive) || - hasBreakpoint(breakpoint, ['extraSmall', 'smallest']) - ) + props.partOfFullPageSet = partOfFullPageSet + props.showAudioTopBorder = audioState.showAudioTopBorder(currentWindow, frameKey, isPinned) + props.centralizeTabIcons = tabUIState.centralizeTabIcons(currentWindow, frameKey, isPinned) + props.gradientColor = tabUIState.getTabEndIconBackgroundColor(currentWindow, frameKey) // used in other functions - props.totalTabs = state.get('tabs').size props.dragData = state.getIn(['dragData', 'type']) === dragTypes.TAB && state.get('dragData') - props.hasTabInFullScreen = tabContentState.hasTabInFullScreen(currentWindow) - props.tabId = frame.get('tabId', tabState.TAB_ID_NONE) + props.tabId = tabId props.previewMode = currentWindow.getIn(['ui', 'tabs', 'previewMode']) return props @@ -280,7 +279,7 @@ class Tab extends React.Component { render () { // we don't want themeColor if tab is private const perPageStyles = !this.props.isPrivateTab && StyleSheet.create({ - themeColor: { + tab_themeColor: { color: this.props.themeColor ? getTextColorForBackground(this.props.themeColor) : 'inherit', background: this.props.themeColor ? this.props.themeColor : 'inherit', ':hover': { @@ -289,6 +288,13 @@ class Tab extends React.Component { } } }) + const perPageGradient = StyleSheet.create({ + tab_gradient: { + '::after': { + background: this.props.gradientColor + } + } + }) return
{ this.tabNode = node }} className={css( styles.tab, + !this.props.isPinnedTab && perPageGradient.tab_gradient, // Windows specific style - isWindows && styles.tabForWindows, - this.props.isPinnedTab && styles.isPinned, - this.props.isActive && styles.active, - this.props.isPlayIndicatorBreakpoint && this.props.canPlayAudio && styles.narrowViewPlayIndicator, - this.props.isActive && this.props.themeColor && perPageStyles.themeColor, + isWindows && styles.tab_forWindows, + this.props.isPinnedTab && styles.tab_pinned, + this.props.isActive && styles.tab_active, + this.props.isActive && this.props.themeColor && perPageStyles.tab_themeColor, + this.props.showAudioTopBorder && styles.tab_audioTopBorder, // Private color should override themeColor - this.props.isPrivateTab && styles.private, - this.props.isActive && this.props.isPrivateTab && styles.activePrivateTab, - !this.props.isPinnedTab && this.props.isNarrowView && styles.tabNarrowView, - !this.props.isPinnedTab && this.props.isNarrowestView && styles.tabNarrowestView, - !this.props.isPinnedTab && this.props.breakpoint === 'smallest' && styles.tabMinAllowedSize + this.props.isPrivateTab && styles.tab_private, + this.props.isActive && this.props.isPrivateTab && styles.tab_active_private, + this.props.centralizeTabIcons && styles.tab__content_centered )} data-test-id='tab' - data-test-active-tab={this.props.isActive} - data-test-pinned-tab={this.props.isPinnedTab} - data-test-private-tab={this.props.isPrivateTab} data-frame-key={this.props.frameKey} draggable title={this.props.title} + onDrag={this.onDrag} onDragStart={this.onDragStart} onDragEnd={this.onDragEnd} onDragOver={this.onDragOver} onClick={this.onClickTab} onContextMenu={contextMenus.onTabContextMenu.bind(this, this.frame)} > +
{ this.tabSentinel = node }} + className={css(styles.tab__sentinel)} + />
- { - this.props.showFavIcon - ? - : null - } - { - this.props.showAudioIcon - ? - : null - } - { - this.props.showTitle - ? - : null - } + styles.tab__identity, + this.props.centralizeTabIcons && styles.tab__content_centered + )}> + + +
- { - this.props.showPrivateIcon - ? - : null - } - { - this.props.showSessionIcon - ? - : null - } - + + +
} } +const styles = StyleSheet.create({ + tab: { + borderWidth: '0 1px 0 0', + borderStyle: 'solid', + borderColor: '#bbb', + boxSizing: 'border-box', + color: theme.tab.color, + display: 'flex', + transition: theme.tab.transition, + height: '-webkit-fill-available', + width: '-webkit-fill-available', + alignItems: 'center', + justifyContent: 'space-between', + position: 'relative', + + ':hover': { + background: theme.tab.hover.background + }, + + // this enable us to have gradient text + '::after': { + zIndex: globalStyles.zindex.zindexTabs, + content: '""', + position: 'absolute', + bottom: 0, + right: 0, + width: '-webkit-fill-available', + height: '-webkit-fill-available' + } + }, + + // Windows specific style + tab_forWindows: { + color: theme.tab.forWindows.color + }, + + tab_pinned: { + padding: 0, + width: '28px', + justifyContent: 'center' + }, + + tab_active: { + background: theme.tab.active.background, + + ':hover': { + background: theme.tab.active.background + } + }, + + tab_audioTopBorder: { + '::before': { + zIndex: globalStyles.zindex.zindexTabsAudioTopBorder, + content: `''`, + display: 'block', + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: '2px', + background: 'lightskyblue' + } + }, + + // The sentinel is responsible to respond to tabs + // intersection state. This is an empty hidden element + // which `width` value shouldn't be changed unless the intersection + // point needs to be edited. + tab__sentinel: { + position: 'absolute', + left: 0, + height: '1px', + background: 'transparent', + width: globalStyles.spacing.sentinelSize + }, + + tab__identity: { + justifyContent: 'flex-start', + alignItems: 'center', + overflow: 'hidden', + display: 'flex', + flex: '1', + minWidth: '0', // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1108514#c5 + margin: `0 ${globalStyles.spacing.defaultTabMargin}` + }, + + tab__content_centered: { + justifyContent: 'center', + flex: 'auto', + padding: 0, + margin: 0 + }, + + tab_active_private: { + background: theme.tab.active.private.background, + color: theme.tab.active.private.color, + + ':hover': { + background: theme.tab.active.private.background + } + }, + + tab_private: { + background: theme.tab.private.background, + + ':hover': { + color: theme.tab.active.private.color, + background: theme.tab.active.private.background + } + } +}) + module.exports = ReduxComponent.connect(Tab) diff --git a/app/renderer/components/tabs/tabs.js b/app/renderer/components/tabs/tabs.js index dce931ee2da..df8a54b2e14 100644 --- a/app/renderer/components/tabs/tabs.js +++ b/app/renderer/components/tabs/tabs.js @@ -17,7 +17,6 @@ const windowActions = require('../../../../js/actions/windowActions') // State const windowState = require('../../../common/state/windowState') -const tabContentState = require('../../../common/state/tabContentState') // Constants const dragTypes = require('../../../../js/constants/dragTypes') @@ -129,7 +128,7 @@ class Tabs extends React.Component { mergeProps (state, ownProps) { const currentWindow = state.get('currentWindow') - const pageIndex = tabContentState.getPageIndex(currentWindow) + const pageIndex = frameStateUtil.getTabPageIndex(currentWindow) const tabsPerTabPage = Number(getSetting(settings.TABS_PER_PAGE)) const startingFrameIndex = pageIndex * tabsPerTabPage const unpinnedTabs = frameStateUtil.getNonPinnedFrames(currentWindow) || Immutable.List() diff --git a/app/renderer/lib/observerUtil.js b/app/renderer/lib/observerUtil.js new file mode 100644 index 00000000000..1ea48445000 --- /dev/null +++ b/app/renderer/lib/observerUtil.js @@ -0,0 +1,42 @@ +/* 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 {noIntersection} = require('../../renderer/components/styles/global').intersection + + /** + * Observes an element's against its parentNode intercection. + * This method enable a callback with information about when + * the node should be considerated intercected or not. + * @param {Object} node - The DOM node to get the parentNode to be used as a root + * @param {Number|String|Array} threshold - Intersection point that will fire the callback + * @returns {Function} Callback with options to be fired when observable passes the threshold. + */ + module.exports.setObserver = (node, threshold, rootMargin = null, cb) => { + const options = { + // We always rely on element's parentNode. original API defaults to window + root: node.parentNode, + // Threshold at 0 means element is fully hidden and 1 means fully visible + threshold: threshold, + // rootMargin is an optional gutter to include in the root element + // such as padding or margin. Accepts default CSS convention TOP RIGHT DOWN LEFT. + // As of Chrome 60, needs units such as pixel or percentage to work + rootMargin: rootMargin + } + return new window.IntersectionObserver(cb, options) + } + + /** + * Checks whether or not the entry in question is being intersected by the parent + * @param {Object} state - The application's current window state + * @param {string} component - The component to watch intersection + * @param {Number|null} ratio - the intersection ratio to listen for + */ + module.exports.isEntryIntersected = (state, component, ratio = null) => { + // intersectionRatio === 1 means the element is not being intercected + // so if ratio is undefined check for a minimum intersection + if (ratio == null) { + return state.getIn(['ui', component, 'intersectionRatio']) < noIntersection + } + return state.getIn(['ui', component, 'intersectionRatio']) <= ratio + } diff --git a/app/renderer/lib/tabUtil.js b/app/renderer/lib/tabUtil.js index dcda77921cb..3ee963e5fc4 100644 --- a/app/renderer/lib/tabUtil.js +++ b/app/renderer/lib/tabUtil.js @@ -2,41 +2,6 @@ * 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 styles = require('../components/styles/global') - -/** - * Get tab's breakpoint name for current tab size. - * @param {Number} tabWidth current tab size - * @returns {String} The matching breakpoint. - */ -module.exports.getTabBreakpoint = (tabWidth) => { - const sizes = ['default', 'large', 'largeMedium', 'medium', 'mediumSmall', 'small', 'extraSmall', 'smallest'] - let currentSize - - sizes.map(size => { - if (tabWidth <= Number.parseInt(styles.breakpoint.tab[size], 10)) { - currentSize = size - return false - } - return true - }) - return currentSize -} - -// Execute resize handler at a rate of 15fps -module.exports.tabUpdateFrameRate = 66 - -/** - * Check whether or not current breakpoint match defined criteria - * @param {Object} breakpoint - Break point value - * @param {Array} arr - Array of Strings including breakpoint names to check against - * @returns {Boolean} Whether or not the sizing criteria was match - */ -module.exports.hasBreakpoint = (breakpoint, arr) => { - arr = Array.isArray(arr) ? arr : [arr] - return arr.includes(breakpoint) -} - /** * Check whether or not the related target is a tab * by checking the parentNode dataset diff --git a/app/renderer/reducers/tabContentReducer.js b/app/renderer/reducers/tabContentReducer.js new file mode 100644 index 00000000000..58de6b18c09 --- /dev/null +++ b/app/renderer/reducers/tabContentReducer.js @@ -0,0 +1,30 @@ +/* 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/. */ + + 'use strict' + + const frameStateUtil = require('../../../js/state/frameStateUtil') + const {makeImmutable} = require('../../common/state/immutableUtil') + const windowConstants = require('../../../js/constants/windowConstants') + + const tabContentReducer = (state, action, immutableAction) => { + action = immutableAction || makeImmutable(action) + switch (action.get('actionType')) { + case windowConstants.WINDOW_SET_TAB_CONTENT_INTERSECTION_STATE: + const firstTabOfTabSet = frameStateUtil + .isFirstFrameKeyInTabPage(state, action.get('frameKey')) + + // since all unpinned tabs in a tabPage share the same size, + // only computes the intersection state for the first tab in the current tab set. + // this gives us a huge performance boost. + if (firstTabOfTabSet) { + state = state.setIn(['ui', 'tabs', 'intersectionRatio'], action.get('ratio')) + break + } + break + } + return state + } + + module.exports = tabContentReducer diff --git a/docs/state.md b/docs/state.md index 42fc712da63..2b52319ffac 100644 --- a/docs/state.md +++ b/docs/state.md @@ -438,7 +438,6 @@ WindowStore audioMuted: boolean, // frame is muted audioPlaybackActive: boolean, // frame is playing audio basicAuthDetail: object, - breakpoint: string, // breakpoint name for current tab size, specified in app/renderer/components/styles/tab.js closedAtIndex: number, // index the frame was last closed at, cleared unless the frame is inside of closedFrames computedThemeColor: string, // CSS computed theme color from the favicon endtLoadTime: datetime, @@ -660,6 +659,7 @@ WindowStore }, tabs: { hoverTabIndex: number, // index of the current hovered tab + intersectionRatio: number, // at which tab size position the tab sentinel is being intersected previewMode: boolean, // whether or not tab preview should be fired based on mouse idle time previewTabPageIndex: number, // index of the tab being previewed tabPageIndex: number // index of the current tab page diff --git a/js/actions/windowActions.js b/js/actions/windowActions.js index 0ea934a094b..fdd3b2f32b0 100644 --- a/js/actions/windowActions.js +++ b/js/actions/windowActions.js @@ -250,17 +250,11 @@ const windowActions = { }) }, - /** - * Dispatches a message to the store to set the tab breakpoint. - * - * @param {Object} frameKey - the frame key for the webview in question. - * @param {string} breakpoint - the tab breakpoint to change to - */ - setTabBreakpoint: function (frameKey, breakpoint) { + setTabIntersectionState: function (frameKey, ratio) { dispatch({ - actionType: windowConstants.WINDOW_SET_TAB_BREAKPOINT, + actionType: windowConstants.WINDOW_SET_TAB_CONTENT_INTERSECTION_STATE, frameKey, - breakpoint + ratio }) }, diff --git a/js/constants/windowConstants.js b/js/constants/windowConstants.js index e63cc3ea8e9..7412c10c949 100644 --- a/js/constants/windowConstants.js +++ b/js/constants/windowConstants.js @@ -12,9 +12,9 @@ const windowConstants = { WINDOW_SET_FOCUSED_FRAME: _, WINDOW_SET_PREVIEW_TAB_PAGE_INDEX: _, WINDOW_SET_TAB_PAGE_INDEX: _, - WINDOW_SET_TAB_BREAKPOINT: _, WINDOW_SET_TAB_HOVER_STATE: _, WINDOW_SET_TAB_PAGE_HOVER_STATE: _, + WINDOW_SET_TAB_CONTENT_INTERSECTION_STATE: _, WINDOW_TAB_MOVE: _, WINDOW_SET_THEME_COLOR: _, WINDOW_WEBVIEW_LOAD_END: _, diff --git a/js/state/frameStateUtil.js b/js/state/frameStateUtil.js index 4fc1f8d702a..91ac0560d62 100644 --- a/js/state/frameStateUtil.js +++ b/js/state/frameStateUtil.js @@ -1,6 +1,6 @@ /* 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/. */ +* 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') @@ -14,6 +14,7 @@ const webviewActions = require('../actions/webviewActions') // State const {makeImmutable} = require('../../app/common/state/immutableUtil') +const tabState = require('../../app/common/state/tabState') // Utils const {getSetting} = require('../settings') @@ -120,9 +121,9 @@ function getTabIdsByNonPinnedDisplayIndex (state) { .map((frame) => frame.get('tabId')) } - /** - * Obtains the display index for the specified tab ID excluding pins - */ +/** +* Obtains the display index for the specified tab ID excluding pins +*/ function findNonPinnedDisplayIndexForTabId (state, tabId) { return getTabIdsByNonPinnedDisplayIndex(state) .findIndex((displayKey) => displayKey === tabId) @@ -194,6 +195,11 @@ function getIndexByTabId (state, tabId) { return index == null ? -1 : index } +const getTabIdByFrameKey = (state, frameKey) => { + const frame = getFrameByKey(state, frameKey) + return frame && frame.get('tabId', tabState.TAB_ID_NONE) +} + function getActiveFrame (state) { const activeFrameIndex = getActiveFrameIndex(state) return state.get('frames').get(activeFrameIndex) @@ -245,24 +251,24 @@ function getPreviousFrame (state) { } /** - * Obtains the display index for the specified frame key - */ +* Obtains the display index for the specified frame key +*/ function findDisplayIndexForFrameKey (state, key) { return getFrameKeysByDisplayIndex(state).findIndex((displayKey) => displayKey === key) } /** - * Determines if the specified frame was opened from the specified - * ancestorFrameKey. - * - * For example you may go to google.com and open 3 links in new tabs: - * G g1 g2 g3 - * Then you may change to g1 and open another tab: - * G g1 g1.1 g2 g3 - * But then you may go back to google.com and open another tab. - * It should go like so: - * G g1 g1.1 g2 g3 g4 - */ +* Determines if the specified frame was opened from the specified +* ancestorFrameKey. +* +* For example you may go to google.com and open 3 links in new tabs: +* G g1 g2 g3 +* Then you may change to g1 and open another tab: +* G g1 g1.1 g2 g3 +* But then you may go back to google.com and open another tab. +* It should go like so: +* G g1 g1.1 g2 g3 g4 +*/ function isAncestorFrameKey (state, frame, parentFrameKey) { if (!frame || !frame.get('parentFrameKey')) { return false @@ -321,9 +327,9 @@ const frameOptsFromFrame = (frame) => { } /** - * Adds a frame specified by frameOpts and newKey and sets the activeFrameKey - * @return Immutable top level application state ready to merge back in - */ +* Adds a frame specified by frameOpts and newKey and sets the activeFrameKey +* @return Immutable top level application state ready to merge back in +*/ function addFrame (state, frameOpts, newKey, partitionNumber, openInForeground, insertionIndex) { const frames = state.get('frames') @@ -393,9 +399,9 @@ function addFrame (state, frameOpts, newKey, partitionNumber, openInForeground, } /** - * Removes a frame specified by frameProps - * @return Immutable top level application state ready to merge back in - */ +* Removes a frame specified by frameProps +* @return Immutable top level application state ready to merge back in +*/ function removeFrame (state, frameProps, framePropsIndex) { const frames = state.get('frames') let closedFrames = state.get('closedFrames') || Immutable.List() @@ -458,20 +464,38 @@ function getTotalBlocks (frame) { } /** - * Check if frame is pinned or not - */ +* Check if frame is pinned or not +*/ function isPinned (state, frameKey) { const frame = getFrameByKey(state, frameKey) return frame && !!frame.get('pinnedLocation') } +const isFirstFrameKeyInTabPage = (state, frameKey) => { + const pageIndex = getTabPageIndex(state) + const tabsPerTabPage = Number(getSetting(settings.TABS_PER_PAGE)) + const startingFrameIndex = pageIndex * tabsPerTabPage + const unpinnedTabs = getNonPinnedFrames(state) || Immutable.List() + const firstFrame = unpinnedTabs + .slice(startingFrameIndex, startingFrameIndex + tabsPerTabPage).first() + + return firstFrame.get('key') === frameKey +} + +const getTabPageIndex = (state) => { + const tabPageIndex = state.getIn(['ui', 'tabs', 'tabPageIndex'], 0) + const previewTabPageIndex = state.getIn(['ui', 'tabs', 'previewTabPageIndex']) + + return previewTabPageIndex || tabPageIndex +} + /** - * Updates the tab page index to the specified frameProps - * @param state{Object} - Window state - * @param tabId{number} - Tab id for the frame - * @param tabsPerPage{string} - Current setting for tabs per page, with a default value - */ +* Updates the tab page index to the specified frameProps +* @param state{Object} - Window state +* @param tabId{number} - Tab id for the frame +* @param tabsPerPage{string} - Current setting for tabs per page, with a default value +*/ function updateTabPageIndex (state, tabId, tabsPerPage = getSetting(settings.TABS_PER_PAGE)) { const index = getFrameTabPageIndex(state, tabId, tabsPerPage) @@ -594,23 +618,23 @@ const setPreviewTabPageIndex = (state, index, immediate = false) => { } /** - * Defines whether or not a tab should be allowed to preview its content - * based on mouse idle time defined by mouse move in tab.js - * @see windowConstants.WINDOW_TAB_MOUSE_MOVE for information - * on how the data is handled in the store. - * @param state {Object} - Application state - * @param previewMode {Boolean} - Whether or not minimium idle time - * has match the criteria - */ +* Defines whether or not a tab should be allowed to preview its content +* based on mouse idle time defined by mouse move in tab.js +* @see windowConstants.WINDOW_TAB_MOUSE_MOVE for information +* on how the data is handled in the store. +* @param state {Object} - Application state +* @param previewMode {Boolean} - Whether or not minimium idle time +* has match the criteria +*/ const setPreviewMode = (state, previewMode) => { return state.setIn(['ui', 'tabs', 'previewMode'], previewMode) } /** - * Gets the previewMode application state - * @param state {Object} - Application state - * @return Immutable top level application state for previewMode - */ +* Gets the previewMode application state +* @param state {Object} - Application state +* @return Immutable top level application state for previewMode +*/ const getPreviewMode = (state) => { return state.getIn(['ui', 'tabs', 'previewMode']) } @@ -652,36 +676,36 @@ const setTabPageHoverState = (state, tabPageIndex, hoverState) => { } /** - * Checks if the current tab index is being hovered - * @param state {Object} - Application state - * @param frameKey {Number} - The current tab's frameKey - * @return Boolean - wheter or not hoverState is true - */ +* Checks if the current tab index is being hovered +* @param state {Object} - Application state +* @param frameKey {Number} - The current tab's frameKey +* @return Boolean - wheter or not hoverState is true +*/ const getTabHoverState = (state, frameKey) => { const index = getFrameIndex(state, frameKey) return getHoverTabIndex(state) === index } /** - * Gets the hovered tab index state - * This check will return null if no tab is being hovered - * and is used getTabHoverState to check if current index is being hovered. - * If the method to apply for does not know the right index - * this should be used instead of getTabHoverState - * @param state {Object} - Application state - * @return Immutable top level application state for hoverTabIndex - */ +* Gets the hovered tab index state +* This check will return null if no tab is being hovered +* and is used getTabHoverState to check if current index is being hovered. +* If the method to apply for does not know the right index +* this should be used instead of getTabHoverState +* @param state {Object} - Application state +* @return Immutable top level application state for hoverTabIndex +*/ const getHoverTabIndex = (state) => { return state.getIn(['ui', 'tabs', 'hoverTabIndex']) } /** - * Sets the hover state for current tab index in top level state - * @param state {Object} - Application state - * @param frameKey {Number} - The current tab's frameKey - * @param hoverState {Boolean} - True if the current tab is being hovered. - * @return Immutable top level application state for hoverTabIndex - */ +* Sets the hover state for current tab index in top level state +* @param state {Object} - Application state +* @param frameKey {Number} - The current tab's frameKey +* @param hoverState {Boolean} - True if the current tab is being hovered. +* @return Immutable top level application state for hoverTabIndex +*/ const setHoverTabIndex = (state, frameKey, hoverState) => { const frameIndex = getFrameIndex(state, frameKey) if (!hoverState) { @@ -692,13 +716,13 @@ const setHoverTabIndex = (state, frameKey, hoverState) => { } /** - * Gets values from the window setTabHoverState action from the store - * and is used to apply both hoverState and previewFrameKey - * @param state {Object} - Application state - * @param frameKey {Number} - The current tab's frameKey - * @param hoverState {Boolean} - True if the current tab is being hovered. - * @return Immutable top level application state for hoverTabIndex - */ +* Gets values from the window setTabHoverState action from the store +* and is used to apply both hoverState and previewFrameKey +* @param state {Object} - Application state +* @param frameKey {Number} - The current tab's frameKey +* @param hoverState {Boolean} - True if the current tab is being hovered. +* @return Immutable top level application state for hoverTabIndex +*/ const setTabHoverState = (state, frameKey, hoverState, enablePreviewMode) => { const frameIndex = getFrameIndex(state, frameKey) if (frameIndex !== -1) { @@ -709,7 +733,21 @@ const setTabHoverState = (state, frameKey, hoverState, enablePreviewMode) => { return state } +const frameLocationMatch = (frame, location) => { + if (frame == null) { + return false + } + const validFrame = Immutable.Map.isMap(frame) + return validFrame && frame.get('location') === location +} + +const hasFrame = (state, frameKey) => { + const frame = getFrameByKey(state, frameKey) + return frame && !frame.isEmpty() +} + module.exports = { + hasFrame, setTabPageHoverState, setPreviewTabPageIndex, getTabHoverState, @@ -745,6 +783,7 @@ module.exports = { getFrameByKey, getFrameByTabId, getIndexByTabId, + getTabIdByFrameKey, getPartitionNumber, setFrameLastAccessedTime, setActiveFrameKey, @@ -766,9 +805,12 @@ module.exports = { onFindBarHide, getTotalBlocks, isPinned, + isFirstFrameKeyInTabPage, + getTabPageIndex, updateTabPageIndex, isValidClosedFrame, getTabPageCount, getSortedFrameKeys, - frameStatePathByTabId + frameStatePathByTabId, + frameLocationMatch } diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index c7d99c40c0d..2d2bea8055b 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -220,6 +220,7 @@ const applyReducers = (state, action, immutableAction) => [ require('../../app/renderer/reducers/urlBarReducer'), require('../../app/renderer/reducers/frameReducer'), require('../../app/renderer/reducers/contextMenuReducer'), + require('../../app/renderer/reducers/tabContentReducer'), // This should be included even in production builds since you can use // an environment variable to show the Debug menu require('../../app/renderer/reducers/debugReducer') @@ -358,17 +359,6 @@ const doAction = (action) => { windowState = frameStateUtil.updateTabPageIndex(windowState, action.frameProps.get('tabId')) } break - case windowConstants.WINDOW_SET_TAB_BREAKPOINT: - { - if (!action.frameKey) { - break - } - const frameIndex = frameStateUtil.getFrameIndex(windowState, action.frameKey) - if (frameIndex !== -1) { - windowState = windowState.setIn(['frames', frameIndex, 'breakpoint'], action.breakpoint) - } - break - } case windowConstants.WINDOW_TAB_MOUSE_MOVE: { // previewMode is only triggered if mouse is idle over a tab diff --git a/less/main.less b/less/main.less index 819d480a1f1..5b64920513f 100644 --- a/less/main.less +++ b/less/main.less @@ -3,7 +3,7 @@ * You can obtain one at http://mozilla.org/MPL/2.0/. */ @bodyBG: #000; -@windowContainerFG: #f00; +@windowContainerFG: #000; body { background-color: @bodyBG; diff --git a/less/tabs.less b/less/tabs.less index 9ebbdf0e37f..ea20d58b23d 100644 --- a/less/tabs.less +++ b/less/tabs.less @@ -10,7 +10,7 @@ flex: 1; overflow: auto; padding: 0; - height: @tabsToolbarHeight; + height: -webkit-fill-available; position: relative; white-space: nowrap; z-index: @zindexTabs; @@ -59,6 +59,7 @@ position: relative; vertical-align: top; overflow: hidden; + height: -webkit-fill-available; // There's a special case that tabs should span the full width // if there are a full set of them. &:not(.partOfFullPageSet) { @@ -122,14 +123,17 @@ box-sizing: border-box; background: @tabsBackground; display: flex; - height: @tabsToolbarHeight; + // This element is set as border-box so it does not + // take into account the borders as width gutter, so we + // increase its size by 1px to include the top border + height: calc(~'@{tabsToolbarHeight} + 1px'); border-top: 1px solid @tabsToolbarBorderColor; user-select: none; } .tabsToolbarButtons { box-sizing: border-box; - height: @tabHeight; + height: -webkit-fill-available; padding-right: 5px; .browserButton { @@ -226,6 +230,8 @@ } .pinnedTabs { + height: -webkit-fill-available; + box-sizing: border-box; margin-left: 0; margin-top: 0; background: @tabsBackgroundInactive; diff --git a/less/window.less b/less/window.less index a55ee98fa55..e161d1d9864 100644 --- a/less/window.less +++ b/less/window.less @@ -164,3 +164,12 @@ body.fullScreen .banner.fade { a { cursor: pointer; } + +// TODO: Investigate if these are required +// See PR #10861, Issue #10877 +body { + background-color: #000; +} +#windowContainer { + color: #000; +} diff --git a/test/unit/app/browser/reducers/tabContentReducerTest.js b/test/unit/app/browser/reducers/tabContentReducerTest.js new file mode 100644 index 00000000000..e2b6fb46340 --- /dev/null +++ b/test/unit/app/browser/reducers/tabContentReducerTest.js @@ -0,0 +1,63 @@ +/* global describe, it, before, after */ +const mockery = require('mockery') +const Immutable = require('immutable') +const assert = require('assert') +const fakeElectron = require('../../../lib/fakeElectron') + +const windowConstants = require('../../../../../js/constants/windowConstants') +require('../../../braveUnit') + +describe('tabContentReducer', function () { + let tabContentReducer + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + tabContentReducer = require('../../../../../app/renderer/reducers/tabContentReducer') + }) + + after(function () { + mockery.disable() + }) + describe('WINDOW_SET_TAB_CONTENT_INTERSECTION_STATE', function () { + const frameKey = 1 + const originalRatio = 0.7 + const newRatio = 1337 + + before(function () { + this.action = Immutable.fromJS({ + actionType: windowConstants.WINDOW_SET_TAB_CONTENT_INTERSECTION_STATE, + frameKey: frameKey, + ratio: originalRatio + }) + this.state = Immutable.fromJS({ + frames: [{ + key: frameKey, + location: 'http://clifton-loves-the-kaisen-philosophy.com' + }], + ui: { tabs: { intersectionRatio: originalRatio } } + }) + }) + + it('sets the intersection ratio if current tab is the first in the first tabPage', function () { + this.action = this.action.set('ratio', newRatio) + this.newState = tabContentReducer(this.state, this.action) + const result = this.newState.getIn(['ui', 'tabs', 'intersectionRatio']) + assert.equal(result, newRatio) + }) + + it('does not set the intersection if current tab is not the first in a tab page', function () { + this.action = this.action.set('ratio', newRatio) + this.state = this.state.mergeIn(['frames', 0], { + key: 2, + location: 'something' + }) + this.newState = tabContentReducer(this.state, this.action) + const result = this.newState.getIn(['ui', 'tabs', 'intersectionRatio']) + assert.equal(result, originalRatio) + }) + }) +}) diff --git a/test/unit/app/common/state/tabContentStateTest.js b/test/unit/app/common/state/tabContentStateTest.js deleted file mode 100644 index 9b9bbec5928..00000000000 --- a/test/unit/app/common/state/tabContentStateTest.js +++ /dev/null @@ -1,545 +0,0 @@ -/* 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/. */ - -/* global describe, it, before, beforeEach, after, afterEach */ - -const assert = require('assert') -const Immutable = require('immutable') -const mockery = require('mockery') -const sinon = require('sinon') -const fakeElectron = require('../../../lib/fakeElectron') -const {braveExtensionId} = require('../../../../../js/constants/config') -const styles = require('../../../../../app/renderer/components/styles/global') - -const frameKey = 1 -const index = 0 -const defaultWindowStore = Immutable.fromJS({ - activeFrameKey: frameKey, - frames: [{ - key: frameKey, - tabId: 1, - location: 'http://brave.com' - }], - tabs: [{ - key: frameKey, - index: index - }], - framesInternal: { - index: { - 1: 0 - }, - tabIndex: { - 1: 0 - } - }, - ui: { - tabs: { - hoverTabIndex: index - } - } -}) - -describe('tabContentState unit tests', function () { - let tabContentState - let frameStateUtil - let getFrameByKeyMock - let defaultValue - let isFrameKeyActive - - before(function () { - mockery.enable({ - warnOnReplace: false, - warnOnUnregistered: false, - useCleanCache: true - }) - frameStateUtil = require('../../../../../js/state/frameStateUtil') - mockery.registerMock('electron', fakeElectron) - mockery.registerMock('../../../js/l10n', { - translation: () => 'translated' - }) - mockery.registerMock('../../../js/state/frameStateUtil', frameStateUtil) - mockery.registerMock('../../../js/settings', { - getSetting: () => defaultValue - }) - tabContentState = require('../../../../../app/common/state/tabContentState') - }) - - beforeEach(function () { - defaultValue = true - }) - - after(function () { - mockery.deregisterAll() - mockery.disable() - }) - - afterEach(function () { - if (getFrameByKeyMock) { - getFrameByKeyMock.restore() - getFrameByKeyMock = undefined - } - - if (isFrameKeyActive) { - isFrameKeyActive.restore() - isFrameKeyActive = undefined - } - }) - - describe('getDisplayTitle', function () { - it('should return empty string if frame is not found', function * () { - const result = tabContentState.getDisplayTitle(defaultWindowStore, 0) - assert.equal(result, '') - }) - - it('should return translated title for about:blank', function * () { - const windowStore = defaultWindowStore.mergeIn(['frames', 0], { - location: 'about:blank' - }) - const result = tabContentState.getDisplayTitle(windowStore, frameKey) - assert.equal(result, 'translated') - }) - - it('should return translated title for about:newtab', function * () { - const windowStore = defaultWindowStore.mergeIn(['frames', 0], { - location: 'about:blank' - }) - const result = tabContentState.getDisplayTitle(windowStore, frameKey) - assert.equal(result, 'translated') - }) - - it('should return title', function * () { - const title = 'Brave' - const windowStore = defaultWindowStore.mergeIn(['frames', 0], { - title: title - }) - const result = tabContentState.getDisplayTitle(windowStore, frameKey) - assert.equal(result, title) - }) - - it('should return location if title is not provided', function * () { - const result = tabContentState.getDisplayTitle(defaultWindowStore, frameKey) - assert.equal(result, defaultWindowStore.getIn(['frames', 0, 'location'])) - }) - - it('should replace play indicator from the title (added by Youtube)', function * () { - const windowStore = defaultWindowStore.mergeIn(['frames', 0], { - title: '▶ Brave' - }) - const result = tabContentState.getDisplayTitle(windowStore, frameKey) - assert.equal(result, 'Brave') - }) - }) - - describe('isTabLoading', function () { - it('handles frame being null/undefined', function () { - assert.equal(tabContentState.isTabLoading(), false) - }) - - describe('when provisionalLocation is not set', function () { - it('returns true if frame.loading', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', (state, frameKey) => { - return Immutable.fromJS({loading: true}) - }) - assert.equal(tabContentState.isTabLoading(), true) - }) - it('returns true if location is about:blank', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', (state, frameKey) => { - return Immutable.fromJS({location: 'about:blank'}) - }) - assert.equal(tabContentState.isTabLoading(), true) - }) - }) - - describe('when provisionalLocation is set', function () { - it('returns false if loading and provisionalLocation is a brave about page', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', (state, frameKey) => { - return Immutable.fromJS({ - loading: true, - provisionalLocation: `chrome-extension://${braveExtensionId}/pageGoesHere` - }) - }) - assert.equal(tabContentState.isTabLoading(), false) - }) - it('returns true if loading and provisionalLocation is not a brave about page', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', (state, frameKey) => { - return Immutable.fromJS({ - loading: true, - provisionalLocation: 'https://brave.com' - }) - }) - assert.equal(tabContentState.isTabLoading(), true) - }) - }) - }) - - describe('isMediumView', function () { - it('handles frame being null/undefined', function () { - assert.equal(tabContentState.isMediumView(), false) - }) - - it('returns true if valid', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', (state, frameKey) => { - return Immutable.fromJS({breakpoint: 'large'}) - }) - assert.equal(tabContentState.isMediumView(), true) - }) - }) - - describe('isNarrowView', function () { - it('returns false if null/undefined', function () { - assert.equal(tabContentState.isNarrowView(), false) - }) - - it('returns true if valid', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', (state, frameKey) => { - return Immutable.fromJS({breakpoint: 'small'}) - }) - assert.equal(tabContentState.isNarrowView(), true) - }) - }) - - describe('isNarrowestView', function () { - it('handles frame being null/undefined', function () { - assert.equal(tabContentState.isNarrowestView(), false) - }) - - it('returns true if valid', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', (state, frameKey) => { - return Immutable.fromJS({breakpoint: 'extraSmall'}) - }) - assert.equal(tabContentState.isNarrowestView(), true) - }) - }) - - describe('getThemeColor', function () { - it('handles frame being null/undefined', function () { - assert.equal(tabContentState.getThemeColor(), false) - }) - - it('if PAINT_TABS is false', function () { - defaultValue = false - assert.equal(tabContentState.getThemeColor(), false) - }) - - it('if PAINT_TABS is true, but dont have themeColor', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return null - }) - assert.equal(tabContentState.getThemeColor(), false) - }) - - it('if PAINT_TABS is true and have themeColor', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - themeColor: '#F00' - }) - }) - assert.equal(tabContentState.getThemeColor(), '#F00') - }) - - it('if PAINT_TABS is true and have computedThemeColor', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - computedThemeColor: '#FFF' - }) - }) - assert.equal(tabContentState.getThemeColor(), '#FFF') - }) - - it('if PAINT_TABS is true and both theme colors are provided', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - themeColor: '#F00', - computedThemeColor: '#FFF' - }) - }) - assert.equal(tabContentState.getThemeColor(), '#F00') - }) - }) - - describe('canPlayAudio', function () { - it('handles frame being null/undefined', function () { - assert.equal(tabContentState.canPlayAudio(), false) - }) - - it('if audioPlaybackActive and audioMuted is not defined', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return null - }) - assert.equal(tabContentState.canPlayAudio(), false) - }) - - it('if audioPlaybackActive is true', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - audioPlaybackActive: true - }) - }) - assert.equal(tabContentState.canPlayAudio(), true) - }) - - it('if audioMuted is true', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - audioMuted: true - }) - }) - assert.equal(tabContentState.canPlayAudio(), true) - }) - - it('if both provided', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - audioPlaybackActive: true, - audioMuted: false - }) - }) - assert.equal(tabContentState.canPlayAudio(), true) - }) - }) - - describe('getPageIndex', function () { - it('handles frame being null/undefined', function () { - const state = Immutable.fromJS({}) - assert.equal(tabContentState.getPageIndex(state), 0) - }) - - it('tabPageIndex is provided', function () { - const state = Immutable.fromJS({ - ui: { - tabs: { - tabPageIndex: 1 - } - } - }) - assert.equal(tabContentState.getPageIndex(state), 1) - }) - - it('previewTabPageIndex is provided', function () { - const state = Immutable.fromJS({ - ui: { - tabs: { - previewTabPageIndex: 1 - } - } - }) - assert.equal(tabContentState.getPageIndex(state), 1) - }) - - it('both are provided', function () { - const state = Immutable.fromJS({ - ui: { - tabs: { - previewTabPageIndex: 1, - tabPageIndex: 2 - } - } - }) - assert.equal(tabContentState.getPageIndex(state), 1) - }) - }) - - describe('getTabIconColor', function () { - it('handles frame being null/undefined', function () { - const state = Immutable.fromJS({}) - assert.equal(tabContentState.getTabIconColor(state), '') - }) - - it('tab is private and active', function () { - const state = defaultWindowStore - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - isPrivate: true - }) - }) - isFrameKeyActive = sinon.stub(frameStateUtil, 'isFrameKeyActive', () => { - return true - }) - assert.equal(tabContentState.getTabIconColor(state), styles.color.white100) - }) - - it('tab is not private and active, but paint is disabled', function () { - const state = defaultWindowStore - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - isPrivate: false - }) - }) - isFrameKeyActive = sinon.stub(frameStateUtil, 'isFrameKeyActive', () => { - return true - }) - defaultValue = false - assert.equal(tabContentState.getTabIconColor(state), styles.color.black100) - }) - - it('all valid', function () { - const state = defaultWindowStore - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - isPrivate: false, - themeColor: '#F00' - }) - }) - isFrameKeyActive = sinon.stub(frameStateUtil, 'isFrameKeyActive', () => { - return true - }) - assert.equal(tabContentState.getTabIconColor(state), 'white') - }) - }) - - describe('hasFixedCloseIcon', function () { - it('handles frame being null/undefined', function () { - const state = Immutable.fromJS({}) - assert.equal(tabContentState.hasFixedCloseIcon(state), false) - }) - - it('frame is not active', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'default' - }) - }) - isFrameKeyActive = sinon.stub(frameStateUtil, 'isFrameKeyActive', () => { - return false - }) - assert.equal(tabContentState.hasFixedCloseIcon(), false) - }) - - it('frame is active and breakpoint is small', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'small' - }) - }) - isFrameKeyActive = sinon.stub(frameStateUtil, 'isFrameKeyActive', () => { - return true - }) - assert.equal(tabContentState.hasFixedCloseIcon(), true) - }) - - it('frame is active and breakpoint is default', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'default' - }) - }) - isFrameKeyActive = sinon.stub(frameStateUtil, 'isFrameKeyActive', () => { - return true - }) - assert.equal(tabContentState.hasFixedCloseIcon(), false) - }) - }) - - describe('hasRelativeCloseIcon', function () { - it('handles frame being null/undefined', function () { - const state = Immutable.fromJS({}) - assert.equal(tabContentState.hasRelativeCloseIcon(state), false) - }) - - it('if not hovering (tabIndex !== hoverTabIndex)', function () { - const state = defaultWindowStore.setIn(['ui', 'tabs', 'hoverTabIndex'], null) - assert.equal(tabContentState.hasRelativeCloseIcon(state, frameKey), false) - }) - - it('if hovering (tabIndex === hoverTabIndex) and break point is small', function () { - const state = defaultWindowStore - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'small' - }) - }) - assert.equal(tabContentState.hasRelativeCloseIcon(state, frameKey), false) - }) - - it('if hovering (tabIndex === hoverTabIndex) and break point is default', function () { - const state = defaultWindowStore - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'default' - }) - }) - assert.equal(tabContentState.hasRelativeCloseIcon(state, frameKey), true) - }) - }) - - describe('hasVisibleSecondaryIcon', function () { - let hasRelativeCloseIcon, hasFixedCloseIcon - - afterEach(function () { - if (hasRelativeCloseIcon) { - hasRelativeCloseIcon.restore() - hasRelativeCloseIcon = undefined - } - if (hasFixedCloseIcon) { - hasFixedCloseIcon.restore() - hasFixedCloseIcon = undefined - } - }) - - it('handles frame being null/undefined', function () { - const state = Immutable.fromJS({}) - assert.equal(tabContentState.hasVisibleSecondaryIcon(state), false) - }) - - it('hasRelativeCloseIcon, dont have hasFixedCloseIcon and is default', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'default' - }) - }) - hasRelativeCloseIcon = sinon.stub(tabContentState, 'hasRelativeCloseIcon', () => { - return true - }) - hasFixedCloseIcon = sinon.stub(tabContentState, 'hasFixedCloseIcon', () => { - return false - }) - assert.equal(tabContentState.hasVisibleSecondaryIcon(), false) - }) - - it('dont have hasRelativeCloseIcon, have hasFixedCloseIcon and is default', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'default' - }) - }) - hasRelativeCloseIcon = sinon.stub(tabContentState, 'hasRelativeCloseIcon', () => { - return false - }) - hasFixedCloseIcon = sinon.stub(tabContentState, 'hasFixedCloseIcon', () => { - return true - }) - assert.equal(tabContentState.hasVisibleSecondaryIcon(), false) - }) - - it('dont have hasRelativeCloseIcon, dont have hasFixedCloseIcon and is small', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'small' - }) - }) - hasRelativeCloseIcon = sinon.stub(tabContentState, 'hasRelativeCloseIcon', () => { - return false - }) - hasFixedCloseIcon = sinon.stub(tabContentState, 'hasFixedCloseIcon', () => { - return false - }) - assert.equal(tabContentState.hasVisibleSecondaryIcon(), false) - }) - - it('dont have hasRelativeCloseIcon, dont have hasFixedCloseIcon and is default', function () { - getFrameByKeyMock = sinon.stub(frameStateUtil, 'getFrameByKey', () => { - return Immutable.fromJS({ - breakpoint: 'default' - }) - }) - hasRelativeCloseIcon = sinon.stub(tabContentState, 'hasRelativeCloseIcon', () => { - return false - }) - hasFixedCloseIcon = sinon.stub(tabContentState, 'hasFixedCloseIcon', () => { - return false - }) - assert.equal(tabContentState.hasVisibleSecondaryIcon(), true) - }) - }) -}) diff --git a/test/unit/app/common/state/tabContentStateTest/audioStateTest.js b/test/unit/app/common/state/tabContentStateTest/audioStateTest.js new file mode 100644 index 00000000000..19aec879344 --- /dev/null +++ b/test/unit/app/common/state/tabContentStateTest/audioStateTest.js @@ -0,0 +1,164 @@ +/* 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/. */ + +/* global describe, it, before, after */ + +const assert = require('assert') +const Immutable = require('immutable') +const mockery = require('mockery') +const fakeElectron = require('../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../app/renderer/components/styles/global') + +const frameKey = 1 +const index = 0 +let defaultState = Immutable.fromJS({ + activeFrameKey: frameKey, + frames: [{ + key: frameKey, + tabId: 1, + location: 'http://brave.com' + }], + tabs: [{ + key: frameKey, + index: index + }], + framesInternal: { + index: { 1: 0 }, + tabIndex: { 1: 0 } + } +}) + +describe('audioState unit tests', function () { + let audioState + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + audioState = require('../../../../../../app/common/state/tabContentState/audioState') + }) + + after(function () { + mockery.deregisterAll() + mockery.disable() + }) + + describe('canPlayAudio', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(audioState.canPlayAudio(), false) + }) + + it('returns true if audioPlaybackActive is true', function * () { + const state = defaultState.setIn(['frames', index, 'audioPlaybackActive'], true) + const result = audioState.canPlayAudio(state, frameKey) + assert.equal(result, true) + }) + + it('returns true if audioMuted is true', function * () { + const state = defaultState.setIn(['frames', index, 'audioMuted'], true) + const result = audioState.canPlayAudio(state, frameKey) + assert.equal(result, true) + }) + + it('returns true if both provided', function * () { + const state = defaultState + .setIn(['frames', index, 'audioMuted'], true) + .setIn(['frames', index, 'audioPlaybackActive'], true) + const result = audioState.canPlayAudio(state, frameKey) + assert.equal(result, true) + }) + }) + + describe('isAudioMuted', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(audioState.isAudioMuted(), false) + }) + + it('returns true if audioMuted is true', function * () { + const state = defaultState.setIn(['frames', index, 'audioMuted'], true) + const result = audioState.isAudioMuted(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if audioMuted is false', function * () { + const state = defaultState.setIn(['frames', index, 'audioMuted'], false) + const result = audioState.isAudioMuted(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('showAudioIcon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(audioState.showAudioIcon(), false) + }) + + it('returns true if tab can play audio and tab is not intercected', function * () { + const state = defaultState + .setIn(['frames', index, 'audioPlaybackActive'], true) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.noIntersection) + const result = audioState.showAudioIcon(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab can play audio and tab is intercected', function * () { + const state = defaultState + .setIn(['frames', index, 'audioPlaybackActive'], true) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = audioState.showAudioIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if tab can not play audio and tab is not intercected', function * () { + const state = defaultState + .setIn(['frames', index, 'audioPlaybackActive'], false) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = audioState.showAudioIcon(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('showAudioTopBorder', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(audioState.showAudioTopBorder(), false) + }) + + it('returns true if tab can play audio and tab is not pinned but is intercected', function * () { + const state = defaultState + .setIn(['frames', index, 'audioPlaybackActive'], true) + .setIn(['frames', index, 'audioMuted'], false) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = audioState.showAudioTopBorder(state, frameKey, false) + assert.equal(result, true) + }) + + it('returns true if tab can play audio and is pinned', function * () { + const state = defaultState + .setIn(['frames', index, 'audioPlaybackActive'], true) + .setIn(['frames', index, 'audioMuted'], false) + const result = audioState.showAudioTopBorder(state, frameKey, true) + assert.equal(result, true) + }) + + it('returns false if tab can play audio and tab is not pinned and is not intercected', function * () { + const state = defaultState + .setIn(['frames', index, 'audioPlaybackActive'], true) + .setIn(['frames', index, 'audioMuted'], false) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.noIntersection) + const result = audioState.showAudioTopBorder(state, frameKey, false) + assert.equal(result, false) + }) + + it('returns false if tab can not play audio, not pinned and tab is intercected', function * () { + const state = defaultState + .setIn(['frames', index, 'audioPlaybackActive'], false) + .setIn(['frames', index, 'audioMuted'], false) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = audioState.showAudioTopBorder(state, frameKey, false) + assert.equal(result, false) + }) + }) +}) diff --git a/test/unit/app/common/state/tabContentStateTest/closeStateTest.js b/test/unit/app/common/state/tabContentStateTest/closeStateTest.js new file mode 100644 index 00000000000..32f44f565c4 --- /dev/null +++ b/test/unit/app/common/state/tabContentStateTest/closeStateTest.js @@ -0,0 +1,165 @@ +/* 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/. */ + +/* global describe, it, before, after */ + +const assert = require('assert') +const Immutable = require('immutable') +const mockery = require('mockery') +const fakeElectron = require('../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../app/renderer/components/styles/global') + +const frameKey = 1 +const index = 0 +let defaultState = Immutable.fromJS({ + activeFrameKey: frameKey, + frames: [{ + key: frameKey, + tabId: 1, + location: 'http://brave.com' + }], + tabs: [{ + key: frameKey, + index: index + }], + framesInternal: { + index: { 1: 0 }, + tabIndex: { 1: 0 } + } +}) + +describe('closeState unit tests', function () { + let closeState + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + closeState = require('../../../../../../app/common/state/tabContentState/closeState') + }) + + after(function () { + mockery.deregisterAll() + mockery.disable() + }) + + describe('hasFixedCloseIcon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(closeState.hasFixedCloseIcon(), false) + }) + + it('returns true if tab is active and is intersected at 75% size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at75) + const result = closeState.hasFixedCloseIcon(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is not active and is intersected at 75% size', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at75) + const result = closeState.hasFixedCloseIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if tab is active and is not intersected', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.noIntersection) + const result = closeState.hasFixedCloseIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if tab is active and intersected below 75% size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = closeState.hasFixedCloseIcon(state, frameKey) + assert.equal(result, true) + }) + }) + + describe('hasRelativeCloseIcon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(closeState.hasRelativeCloseIcon(), false) + }) + + it('returns true if tab index is being hovered', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'hoverTabIndex'], index) + const result = closeState.hasRelativeCloseIcon(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is not being hovered', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'hoverTabIndex'], 1337) + const result = closeState.hasRelativeCloseIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if tab is being intersected', function * () { + const state = defaultState + .mergeIn(['ui', 'tabs'], { + hoverTabIndex: index, + intersectionRatio: intersection.at75 + }) + const result = closeState.hasRelativeCloseIcon(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('showCloseTabIcon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(closeState.showCloseTabIcon(), false) + }) + + it('returns false if tab is being intersected at 15% size', function * () { + const state = defaultState + .mergeIn(['ui', 'tabs'], { + hoverTabIndex: index, + intersectionRatio: intersection.at20 + }) + const result = closeState.showCloseTabIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if tab not intersected and not hovered', function * () { + const state = defaultState + .mergeIn(['ui', 'tabs'], { + hoverTabIndex: 1337, + intersectionRatio: intersection.noIntersection + }) + const result = closeState.showCloseTabIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if tab not intersected and hovered', function * () { + const state = defaultState + .mergeIn(['ui', 'tabs'], { + hoverTabIndex: index, + intersectionRatio: intersection.noIntersection + }) + const result = closeState.showCloseTabIcon(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is intersected and not active', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio', intersection.at45]) + const result = closeState.showCloseTabIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if tab is intersected and active', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at75) + const result = closeState.showCloseTabIcon(state, frameKey) + assert.equal(result, true) + }) + }) +}) diff --git a/test/unit/app/common/state/tabContentStateTest/faviconStateTest.js b/test/unit/app/common/state/tabContentStateTest/faviconStateTest.js new file mode 100644 index 00000000000..ea90d8ed18d --- /dev/null +++ b/test/unit/app/common/state/tabContentStateTest/faviconStateTest.js @@ -0,0 +1,182 @@ +/* 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/. */ + +/* global describe, it, before, after */ + +const assert = require('assert') +const Immutable = require('immutable') +const mockery = require('mockery') +const fakeElectron = require('../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../app/renderer/components/styles/global') + +const frameKey = 1 +const index = 0 +let defaultState = Immutable.fromJS({ + activeFrameKey: frameKey, + frames: [{ + key: frameKey, + tabId: 1, + location: 'http://brave.com' + }], + tabs: [{ + key: frameKey, + index: index + }], + framesInternal: { + index: { 1: 0 }, + tabIndex: { 1: 0 } + } +}) + +describe('faviconState unit tests', function () { + let faviconState + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + faviconState = require('../../../../../../app/common/state/tabContentState/faviconState') + }) + + after(function () { + mockery.deregisterAll() + mockery.disable() + }) + + describe('showFavicon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(faviconState.showFavicon(), false) + }) + + it('returns true if tab is only 35% visible and is not active', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = faviconState.showFavicon(state, frameKey) + assert.equal(result, true) + }) + + it('returns true if tab is not intercected and is not about:newtab', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.noIntersection) + const result = faviconState.showFavicon(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is not intercected and is about:newtab', function * () { + const state = defaultState + .setIn(['frames', index, 'location'], 'about:newtab') + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.noIntersection) + const result = faviconState.showFavicon(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('getFavicon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(faviconState.getFavicon(), false) + }) + + it('returns false if loading icon is visible', function * () { + const favicon = 'fred_water.png' + const state = defaultState.mergeIn(['frames', index], { loading: true, icon: favicon }) + const result = faviconState.getFavicon(state, frameKey) + assert.equal(result, false) + }) + + it('returns the favicon if loading is not visible', function * () { + const favicon = 'fred_water_rlz.png' + const state = defaultState.mergeIn(['frames', index], { loading: false, icon: favicon }) + const result = faviconState.getFavicon(state, frameKey) + assert.equal(result, favicon) + }) + }) + + describe('showLoadingIcon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(faviconState.showLoadingIcon(), false) + }) + + it('returns false if source is about page', function * () { + const state = defaultState + .setIn(['frames', index, 'location'], 'about:blank') + const result = faviconState.showLoadingIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if source is not about page', function * () { + const state = defaultState.setIn(['frames', index, 'loading'], true) + const result = faviconState.showLoadingIcon(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if page is not loading', function * () { + const state = defaultState.setIn(['frames', index, 'loading'], false) + const result = faviconState.showLoadingIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if loading is undefined', function * () { + const state = defaultState.setIn(['frames', index, 'loading'], undefined) + const result = faviconState.showLoadingIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if page is loading', function * () { + const state = defaultState.setIn(['frames', index, 'loading'], true) + const result = faviconState.showLoadingIcon(state, frameKey) + assert.equal(result, true) + }) + }) + + describe('showIconWithLessMargin', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(faviconState.showIconWithLessMargin(), false) + }) + + it('returns true if tab is intersected at 20% size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at30) + const result = faviconState.showIconWithLessMargin(state, frameKey) + assert.equal(result, true) + }) + + it('returns true if tab is intersected at smaller size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at20) + const result = faviconState.showIconWithLessMargin(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is intersected at a larger size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = faviconState.showIconWithLessMargin(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('showFaviconAtReducedSize', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(faviconState.showFaviconAtReducedSize(), false) + }) + + it('returns true if tab is intersected at 15% size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at20) + const result = faviconState.showFaviconAtReducedSize(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is intersected at larger size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at30) + const result = faviconState.showFaviconAtReducedSize(state, frameKey) + assert.equal(result, false) + }) + }) +}) diff --git a/test/unit/app/common/state/tabContentStateTest/partitionStateTest.js b/test/unit/app/common/state/tabContentStateTest/partitionStateTest.js new file mode 100644 index 00000000000..dbef33f2352 --- /dev/null +++ b/test/unit/app/common/state/tabContentStateTest/partitionStateTest.js @@ -0,0 +1,118 @@ +/* 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/. */ + +/* global describe, it, before, after */ + +const assert = require('assert') +const Immutable = require('immutable') +const mockery = require('mockery') +const fakeElectron = require('../../../../lib/fakeElectron') +const {tabs} = require('../../../../../../js/constants/config') + +const frameKey = 1 +const index = 0 +let defaultState = Immutable.fromJS({ + activeFrameKey: frameKey, + frames: [{ + key: frameKey, + tabId: 1, + location: 'http://brave.com' + }], + tabs: [{ + key: frameKey, + index: index + }], + framesInternal: { + index: { 1: 0 }, + tabIndex: { 1: 0 } + } +}) + +describe('partitionState unit tests', function () { + let partitionState + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + partitionState = require('../../../../../../app/common/state/tabContentState/partitionState') + }) + + after(function () { + mockery.deregisterAll() + mockery.disable() + }) + + describe('isPartitionTab', function () { + it('returns false if frame is null/undefined', function () { + assert.equal(partitionState.isPartitionTab(), false) + }) + + it('returns true if partition number is defined', function * () { + const partitionNumber = 1337 + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], partitionNumber) + const result = partitionState.isPartitionTab(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if partition number is undefined', function * () { + const state = defaultState + const result = partitionState.isPartitionTab(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('getPartitionNumber', function () { + it('returns zero if frame is null/undefined', function * () { + assert.equal(partitionState.getPartitionNumber(), 0) + }) + + it('returns zero if frame is null/undefined', function * () { + assert.equal(partitionState.getPartitionNumber(), 0) + }) + + it('can remove _partition_ string and keep the partition number', function * () { + const partitionString = 'partition-9' + const partitionNumber = 9 + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], partitionString) + const result = partitionState.getPartitionNumber(state, frameKey) + assert.equal(result, partitionNumber) + }) + + it('returns the partition number', function * () { + const partitionNumber = 9 + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], partitionNumber) + const result = partitionState.getPartitionNumber(state, frameKey) + assert.equal(result, partitionNumber) + }) + }) + + describe('getMaxAllowedPartitionNumber', function () { + it('returns false if frame is null/undefined', function () { + assert.equal(partitionState.getMaxAllowedPartitionNumber(), false) + }) + + it('returns partition number', function * () { + const partitionNumber = 9 + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], partitionNumber) + const result = partitionState.getMaxAllowedPartitionNumber(state, frameKey) + assert.equal(result, partitionNumber) + }) + + it('returns the max allowed partition number if current number is bigger', function * () { + const partitionNumber = 99999 + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], partitionNumber) + const result = partitionState.getMaxAllowedPartitionNumber(state, frameKey) + assert.equal(result, tabs.maxAllowedNewSessions) + }) + }) +}) diff --git a/test/unit/app/common/state/tabContentStateTest/privateStateTest.js b/test/unit/app/common/state/tabContentStateTest/privateStateTest.js new file mode 100644 index 00000000000..902d94108ae --- /dev/null +++ b/test/unit/app/common/state/tabContentStateTest/privateStateTest.js @@ -0,0 +1,66 @@ +/* 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/. */ + +/* global describe, it, before, after */ + +const assert = require('assert') +const Immutable = require('immutable') +const mockery = require('mockery') +const fakeElectron = require('../../../../lib/fakeElectron') + +const frameKey = 1 +const index = 0 +let defaultState = Immutable.fromJS({ + activeFrameKey: frameKey, + frames: [{ + key: frameKey, + tabId: 1, + location: 'http://brave.com' + }], + tabs: [{ + key: frameKey, + index: index + }], + framesInternal: { + index: { 1: 0 }, + tabIndex: { 1: 0 } + } +}) + +describe('privateState unit tests', function () { + let privateState + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + privateState = require('../../../../../../app/common/state/tabContentState/privateState') + }) + + after(function () { + mockery.deregisterAll() + mockery.disable() + }) + + describe('isPrivateTab', function () { + it('returns an empty string if frame is null/undefined', function () { + assert.equal(privateState.isPrivateTab(), false) + }) + + it('returns true if tab is private', function * () { + const state = defaultState.setIn(['frames', index, 'isPrivate'], true) + const result = privateState.isPrivateTab(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is not private', function * () { + const state = defaultState.setIn(['frames', index, 'isPrivate'], false) + const result = privateState.isPrivateTab(state, frameKey) + assert.equal(result, false) + }) + }) +}) diff --git a/test/unit/app/common/state/tabContentStateTest/titleStateTest.js b/test/unit/app/common/state/tabContentStateTest/titleStateTest.js new file mode 100644 index 00000000000..2c45c294f93 --- /dev/null +++ b/test/unit/app/common/state/tabContentStateTest/titleStateTest.js @@ -0,0 +1,157 @@ +/* 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/. */ + +/* global describe, it, before, after */ + +const assert = require('assert') +const Immutable = require('immutable') +const mockery = require('mockery') +const fakeElectron = require('../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../app/renderer/components/styles/global') + +const frameKey = 1 +const index = 0 +let defaultState = Immutable.fromJS({ + activeFrameKey: frameKey, + frames: [{ + key: frameKey, + tabId: 1, + location: 'http://brave.com' + }], + tabs: [{ + key: frameKey, + index: index + }], + framesInternal: { + index: { 1: 0 }, + tabIndex: { 1: 0 } + } +}) + +describe('titleState unit tests', function () { + let titleState + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('../../../../js/l10n', { + translation: () => 'wow such title very translated' + }) + titleState = require('../../../../../../app/common/state/tabContentState/titleState') + }) + + after(function () { + mockery.deregisterAll() + mockery.disable() + }) + + describe('showTabTitle', function () { + it('returns an empty string if frame is null/undefined', function () { + assert.equal(titleState.showTabTitle(), false) + }) + + it('returns false if tab is intersected at 45% size and is active', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = titleState.showTabTitle(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if tab is intersected at 45% size and is partitioned', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['frames', index, 'partitionNumber'], 1) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = titleState.showTabTitle(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if tab is intersected at 45% size and is private', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['frames', index, 'isPrivate'], true) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = titleState.showTabTitle(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if tab is intersected at 45% size and is about:newtab', function * () { + const state = defaultState + .setIn(['frames', index, 'location'], 'about:newtab') + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = titleState.showTabTitle(state, frameKey) + assert.equal(result, true) + }) + + it('returns true if tab is intersected at 45% size and has no secondary icon', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['frames', index, 'isPrivate'], false) + .setIn(['frames', index, 'partitionNumber'], null) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = titleState.showTabTitle(state, frameKey) + assert.equal(result, true) + }) + + it('returns false if tab is intersected at 35% size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = titleState.showTabTitle(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if tab is intersected above 35% size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at60) + const result = titleState.showTabTitle(state, frameKey) + assert.equal(result, true) + }) + }) + + describe('getDisplayTitle', function () { + it('returns an empty string if frame is null/undefined', function () { + assert.equal(titleState.getDisplayTitle(), false) + }) + + it('translates about:blank', function () { + const state = defaultState.setIn(['frames', index, 'location'], 'about:blank') + const result = titleState.getDisplayTitle(state, frameKey) + assert.equal(result, 'wow such title very translated') + }) + + it('translates about:newtab', function () { + const state = defaultState.setIn(['frames', index, 'location'], 'about:newtab') + const result = titleState.getDisplayTitle(state, frameKey) + assert.equal(result, 'wow such title very translated') + }) + + it('returns the title', function () { + const state = defaultState.setIn(['frames', index, 'title'], 'george clooney') + const result = titleState.getDisplayTitle(state, frameKey) + assert.equal(result, 'george clooney') + }) + + it('returns the location if title is not defined', function () { + const state = defaultState.mergeIn(['frames', index], { + title: '', + location: 'https://i-wouldnt-change-a-thing.com' + }) + const result = titleState.getDisplayTitle(state, frameKey) + assert.equal(result, 'https://i-wouldnt-change-a-thing.com') + }) + + it('retuns an empty string if both title and location are not defined', function () { + const state = defaultState.mergeIn(['frames', index], { + title: '', + location: '' + }) + const result = titleState.getDisplayTitle(state, frameKey) + assert.equal(result, '') + }) + }) +}) diff --git a/test/unit/app/common/state/tabUIStateTest.js b/test/unit/app/common/state/tabUIStateTest.js new file mode 100644 index 00000000000..fe9a9925e3e --- /dev/null +++ b/test/unit/app/common/state/tabUIStateTest.js @@ -0,0 +1,408 @@ +/* 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/. */ + +/* global describe, it, before, beforeEach, after */ + +const assert = require('assert') +const Immutable = require('immutable') +const mockery = require('mockery') +const fakeElectron = require('../../../lib/fakeElectron') +const {theme} = require('../../../../../app/renderer/components/styles/theme') +const {intersection} = require('../../../../../app/renderer/components/styles/global') + +const frameKey = 1 +const index = 0 +let defaultState = Immutable.fromJS({ + activeFrameKey: frameKey, + frames: [{ + key: frameKey, + tabId: 1, + location: 'http://brave.com' + }], + tabs: [{ + key: frameKey, + index: index + }], + framesInternal: { + index: { 1: 0 }, + tabIndex: { 1: 0 } + } +}) + +describe('tabUIState unit tests', function () { + let tabUIState + let defaultValue + + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + mockery.registerMock('../../../js/settings', { + getSetting: () => defaultValue + }) + tabUIState = require('../../../../../app/common/state/tabUIState') + }) + + beforeEach(function () { + defaultValue = true + }) + + after(function () { + mockery.deregisterAll() + mockery.disable() + }) + + describe('getThemeColor', function () { + it('returns an empty string if frame is null/undefined', function * () { + assert.equal(tabUIState.getThemeColor(), false) + }) + + it('returns the themeColor when PAINT_TABS is true', function * () { + const state = defaultState.setIn(['frames', index, 'themeColor'], '#c0ff33') + const result = tabUIState.getThemeColor(state, frameKey) + assert.equal(result, '#c0ff33') + }) + + it('returns computedThemeColor when PAINT_TABS is true and themeColor is empty', function * () { + const state = defaultState.mergeIn(['frames', index], { + themeColor: '', + computedThemeColor: 'saddlebrown' + }) + const result = tabUIState.getThemeColor(state, frameKey) + assert.equal(result, 'saddlebrown') + }) + + it('returns false when PAINT_TABS is false', function * () { + defaultValue = false + const state = defaultState.mergeIn(['frames', index], { + themeColor: '#c0ff33', + computedThemeColor: 'saddlebrown' + }) + const result = tabUIState.getThemeColor(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('getTabIconColor', function () { + it('returns an empty string if frame is null/undefined', function * () { + assert.equal(tabUIState.getTabIconColor(), false) + }) + + it('returns black if tab background is lighter, has themeColor, paintTabs is enabled and is active but not private', function * () { + const state = defaultState.mergeIn(['frames', index], { + themeColor: '#fff', + isPrivate: false + }) + const result = tabUIState.getTabIconColor(state, frameKey) + assert.equal(result, 'black') + }) + + it('returns black if tab background is darker, has themeColor, paintTabs is enabled and is active but not private', function * () { + const state = defaultState.mergeIn(['frames', index], { + themeColor: '#000', + isPrivate: false + }) + const result = tabUIState.getTabIconColor(state, frameKey) + assert.equal(result, 'white') + }) + + it('returns white if tab is active and private', function * () { + const state = defaultState.mergeIn(['frames', index], { + themeColor: '#fff', + isPrivate: true + }) + const result = tabUIState.getTabIconColor(state, frameKey) + assert.equal(result, 'white') + }) + + it('returns black if tab is active, not private but has no themeColor', function * () { + const state = defaultState.mergeIn(['frames', index], { + themeColor: false, + isPrivate: false + }) + const result = tabUIState.getTabIconColor(state, frameKey) + assert.equal(result, 'black') + }) + }) + + describe('checkIfTextColor', function () { + it('returns an empty string if frame is null/undefined', function * () { + assert.equal(tabUIState.checkIfTextColor(), false) + }) + + it('returns true if colors match', function * () { + const state = defaultState.mergeIn(['frames', index], { + themeColor: false, + isPrivate: false + }) + const result = tabUIState.checkIfTextColor(state, frameKey, 'black') + assert.equal(result, true) + }) + + it('returns false if colors does not match', function * () { + const state = defaultState.mergeIn(['frames', index], { + themeColor: false, + isPrivate: true + }) + const result = tabUIState.checkIfTextColor(state, frameKey, 'black') + assert.equal(result, false) + }) + }) + + describe('showTabEndIcon', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(tabUIState.showTabEndIcon(), false) + }) + + it('returns false for regular tabs', function * () { + const state = defaultState.mergeIn(['frames', index], { + isPrivate: false, + partitionNumber: 0 + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false for regular tabs', function * () { + const state = defaultState.mergeIn(['frames', index], { + isPrivate: false, + partitionNumber: false + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + describe('when tab is partitioned', function () { + it('returns false if intersection is above 35% of tab size and has relative close icon', function * () { + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], 1337) + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.at75, + hoverTabIndex: index + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if intersection is above 35% of tab size and has fixed close icon', function * () { + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at75) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if intersection is below 35% of tab size', function * () { + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if not hovering and intersection is above 35% of tab size', function * () { + const state = defaultState + .setIn(['frames', index, 'partitionNumber'], 1337) + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection, + hoverTabIndex: 123123 + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, true) + }) + + it('returns true if not active and intersection is above 35% of tab size', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['frames', index, 'partitionNumber'], 1337) + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, true) + }) + }) + + describe('when tab is private', function () { + it('returns false if intersection is above 35% of tab size and has relative close icon', function * () { + const state = defaultState + .setIn(['frames', index, 'isPrivate'], true) + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.at75, + hoverTabIndex: index + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if intersection is above 35% of tab size and has fixed close icon', function * () { + const state = defaultState + .setIn(['frames', index, 'isPrivate'], true) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at75) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns false if intersection is below 35% of tab size', function * () { + const state = defaultState + .setIn(['frames', index, 'isPrivate'], true) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if not hovering and intersection is above 35% of tab size', function * () { + const state = defaultState + .setIn(['frames', index, 'isPrivate'], true) + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection, + hoverTabIndex: 123123 + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, true) + }) + + it('returns true if not active and intersection is above 35% of tab size', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['frames', index, 'isPrivate'], true) + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection + }) + const result = tabUIState.showTabEndIcon(state, frameKey) + assert.equal(result, true) + }) + }) + }) + + describe('addExtraGutterToTitle', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(tabUIState.addExtraGutterToTitle(), false) + }) + it('returns true for about:newtab', function * () { + const state = defaultState.setIn(['frames', index, 'location'], 'about:newtab') + const result = tabUIState.addExtraGutterToTitle(state, frameKey) + assert.equal(result, true) + }) + it('returns false for other locations', function * () { + const state = defaultState.setIn(['frames', index, 'location'], 'whatelse.com') + const result = tabUIState.addExtraGutterToTitle(state, frameKey) + assert.equal(result, false) + }) + }) + + describe('centralizeTabIcons', function () { + it('returns false if frame is null/undefined', function * () { + assert.equal(tabUIState.centralizeTabIcons(), false) + }) + + it('returns false if intersection is above 15% of tab size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const result = tabUIState.centralizeTabIcons(state, frameKey) + assert.equal(result, false) + }) + + it('returns true if intersection is below or equal 15% of tab size', function * () { + const state = defaultState + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at20) + const result = tabUIState.centralizeTabIcons(state, frameKey) + assert.equal(result, true) + }) + }) + + describe('getTabEndIconBackgroundColor', function () { + before(function () { + // just a helper for results + this.defaultResult = (bgColor, color1Size, color2Size) => + `linear-gradient(to left, ${bgColor} ${color1Size}, transparent ${color2Size})` + }) + + describe('when tab is private', function () { + it('returns `tab.private.background` color if not active', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .mergeIn(['frames', index], { + themeColor: '#c0ff33', + isPrivate: true + }) + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult(theme.tab.private.background, '10px', '40px') + assert.equal(result, expected) + }) + + it('returns `tab.active.private.background` if tab is active', function * () { + const state = defaultState + .mergeIn(['frames', index], { + themeColor: '#c0ff33', + isPrivate: true + }) + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult(theme.tab.active.private.background, '10px', '40px') + assert.equal(result, expected) + }) + + it('retuns active private color if tab is being hovered', function * () { + const state = defaultState + .mergeIn(['frames', index], { + themeColor: '#c0ff33', + isPrivate: true + }) + .setIn(['ui', 'tabs', 'hoverTabIndex'], index) + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult(theme.tab.active.private.background, '10px', '40px') + assert.equal(result, expected) + }) + }) + + describe('when tab is not private', function () { + it('returns the themeColor if tab is active', function * () { + const state = defaultState + .setIn(['frames', index, 'themeColor'], '#c0ff33') + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult('#c0ff33', '0', '12px') + assert.equal(result, expected) + }) + it('returns `theme.tab.background` if tab is not active', function * () { + const state = defaultState + .set('activeFrameKey', 1337) + .setIn(['frames', index, 'themeColor'], '#c0ff33') + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult(theme.tab.background, '0', '12px') + assert.equal(result, expected) + }) + }) + + describe('returns `linear gradient` size', function () { + it('at 10px/40px if tab is partitioned', function * () { + const state = defaultState + .mergeIn(['frames', index], { + partitionNumber: 1337, + themeColor: '#c0ff33' + }) + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult('#c0ff33', '10px', '40px') + assert.equal(result, expected) + }) + it('at 10px/40px gradient size if tab has a visible close icon', function * () { + const state = defaultState + .setIn(['frames', index, 'themeColor'], '#c0ff33') + .setIn(['ui', 'tabs', 'hoverTabIndex'], index) + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult('#c0ff33', '10px', '40px') + assert.equal(result, expected) + }) + it('at 0/12px gradient size if is neither private, partition or has close icon visible', function * () { + const state = defaultState + .setIn(['frames', index, 'themeColor'], '#c0ff33') + const result = tabUIState.getTabEndIconBackgroundColor(state, frameKey) + const expected = this.defaultResult('#c0ff33', '0', '12px') + assert.equal(result, expected) + }) + }) + }) +}) diff --git a/test/unit/app/renderer/components/tabs/content/audioTabIconTest.js b/test/unit/app/renderer/components/tabs/content/audioTabIconTest.js index c142fbb8ef4..98ba8a7a5e9 100644 --- a/test/unit/app/renderer/components/tabs/content/audioTabIconTest.js +++ b/test/unit/app/renderer/components/tabs/content/audioTabIconTest.js @@ -1,36 +1,37 @@ /* 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/. */ -/* global describe, before, it, after */ +/* global describe, before, after, it */ const mockery = require('mockery') const {mount} = require('enzyme') const assert = require('assert') const Immutable = require('immutable') -const globalStyles = require('../../../../../../../app/renderer/components/styles/global') const fakeElectron = require('../../../../../lib/fakeElectron') +const globalStyles = require('../../../../../../../app/renderer/components/styles/global') require('../../../../../braveUnit') +const index = 0 const tabId = 1 const frameKey = 1 -const invalidFrameKey = 71 -const fakeAppStoreRenderer = { - state: Immutable.fromJS({ - windows: [{ - windowId: 1, - windowUUID: 'uuid' - }], - tabs: [{ - tabId: tabId, - windowId: 1, - windowUUID: 'uuid', - url: 'https://brave.com' - }] - }), - addChangeListener: () => {}, - removeChangeListener: () => {} -} +const fakeAppStoreRenderer = Immutable.fromJS({ + windows: [{ + windowId: 1, + windowUUID: 'uuid' + }], + tabs: [{ + tabId: tabId, + windowId: 1, + windowUUID: 'uuid', + url: 'https://brave.com' + }], + tabsInternal: { + index: { + 1: 0 + } + } +}) const defaultWindowStore = Immutable.fromJS({ activeFrameKey: frameKey, @@ -40,7 +41,8 @@ const defaultWindowStore = Immutable.fromJS({ location: 'http://brave.com' }], tabs: [{ - key: frameKey + key: frameKey, + index: index }], framesInternal: { index: { @@ -53,7 +55,7 @@ const defaultWindowStore = Immutable.fromJS({ }) describe('Tabs content - AudioTabIcon', function () { - let Tab, windowStore + let AudioTabIcon, windowStore, appStore before(function () { mockery.enable({ @@ -62,14 +64,10 @@ describe('Tabs content - AudioTabIcon', function () { useCleanCache: true }) mockery.registerMock('electron', fakeElectron) - mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) - mockery.registerMock('../../../../extensions/brave/img/tabs/loading.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/new_session.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/private.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_hover.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_normal.svg') windowStore = require('../../../../../../../js/stores/windowStore') - Tab = require('../../../../../../../app/renderer/components/tabs/tab') + appStore = require('../../../../../../../js/stores/appStoreRenderer') + AudioTabIcon = require('../../../../../../../app/renderer/components/tabs/content/audioTabIcon') + appStore.state = fakeAppStoreRenderer }) after(function () { @@ -77,62 +75,43 @@ describe('Tabs content - AudioTabIcon', function () { mockery.disable() }) - describe('should show', function () { - it('play icon if page has audio enabled', function () { + describe('should show icon', function () { + it('volumeOn if page has audio enabled', function * () { windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - audioPlaybackActive: true, - breakpoint: 'default' + audioPlaybackActive: true }) - const wrapper = mount() - assert.equal(wrapper.find('AudioTabIcon TabIcon').props().symbol, globalStyles.appIcons.volumeOn) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], globalStyles.appIcons.volumeOn) }) - it('mute icon if page has audio muted', function () { + it('volumeOff if page has audio muted', function * () { windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { audioPlaybackActive: true, - audioMuted: true, - breakpoint: 'default' + audioMuted: true }) - const wrapper = mount() - assert.equal(wrapper.find('AudioTabIcon TabIcon').props().symbol, globalStyles.appIcons.volumeOff) - }) - it('passing in a frame key which does not exist does not fail', function () { - windowStore.state = defaultWindowStore - const wrapper = mount() - // No audio icon is rendered in this case so just check for Tab - assert(wrapper.find('Tab')) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], globalStyles.appIcons.volumeOff) }) }) - describe('should not show', function () { - it('any audio icon if page has audio disabled', function () { + describe('should not show icon', function () { + it('if page has audio disabled', function * () { windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { audioPlaybackActive: false, - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('AudioTabIcon').length, 0) - assert.equal(wrapper.find('AudioTabIcon').length, 0) - }) - - it('play audio icon if tab size is different than default', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - audioPlaybackActive: true, - audioMuted: false, - breakpoint: 'small' + audioMuted: false }) - const wrapper = mount() - assert.equal(wrapper.find('AudioTabIcon').length, 0) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('mute icon if tab size is different than default', function () { + it('if tab is intersected', function * () { windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { audioPlaybackActive: true, - audioMuted: true, - breakpoint: 'small' + audioMuted: false }) - const wrapper = mount() - assert.equal(wrapper.find('AudioTabIcon').length, 0) + windowStore.state = defaultWindowStore.setIn(['ui', 'tabs', 'intersection'], 0) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) }) }) diff --git a/test/unit/app/renderer/components/tabs/content/closeTabIconTest.js b/test/unit/app/renderer/components/tabs/content/closeTabIconTest.js index 85455b8432b..716eae7fb54 100644 --- a/test/unit/app/renderer/components/tabs/content/closeTabIconTest.js +++ b/test/unit/app/renderer/components/tabs/content/closeTabIconTest.js @@ -8,29 +8,12 @@ const {mount} = require('enzyme') const assert = require('assert') const Immutable = require('immutable') const fakeElectron = require('../../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../../app/renderer/components/styles/global') require('../../../../../braveUnit') const index = 0 const tabId = 1 const frameKey = 1 -const invalidFrameKey = 71 - -const fakeAppStoreRenderer = { - state: Immutable.fromJS({ - windows: [{ - windowId: 1, - windowUUID: 'uuid' - }], - tabs: [{ - tabId: tabId, - windowId: 1, - windowUUID: 'uuid', - url: 'https://brave.com' - }] - }), - addChangeListener: () => {}, - removeChangeListener: () => {} -} const defaultWindowStore = Immutable.fromJS({ activeFrameKey: frameKey, @@ -50,11 +33,6 @@ const defaultWindowStore = Immutable.fromJS({ tabIndex: { 1: 0 } - }, - ui: { - tabs: { - hoverTabIndex: index - } } }) @@ -68,9 +46,7 @@ describe('Tabs content - CloseTabIcon', function () { useCleanCache: true }) mockery.registerMock('electron', fakeElectron) - mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_hover.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_normal.svg') + mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn.svg') windowStore = require('../../../../../../../js/stores/windowStore') CloseTabIcon = require('../../../../../../../app/renderer/components/tabs/content/closeTabIcon') }) @@ -81,154 +57,46 @@ describe('Tabs content - CloseTabIcon', function () { }) describe('should show icon', function () { - it('if mouse is over tab and breakpoint is default', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: true, - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-on') - }) - - it('if mouse is over tab and breakpoint is large', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: true, - breakpoint: 'large' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-on') - }) - - it('if tab size is largeMedium and tab is active', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: false, - breakpoint: 'largeMedium' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-on') - }) - - it('if tab size is medium and tab is active', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: false, - breakpoint: 'medium' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-on') - }) - - it('if tab size is mediumSmall and tab is active', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: false, - breakpoint: 'mediumSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-on') - }) - - it('if tab size is small and tab is active', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: false, - breakpoint: 'small' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-on') - }) - - it('if tab size is extraSmall and tab is active', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: false, - breakpoint: 'extraSmall' + it('if not intersected and tab is hovered', function * () { + windowStore.state = defaultWindowStore.mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection, + hoverTabIndex: index }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-on') + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 1) }) - it('passing in a frame key which does not exist does not fail', function () { + it('if intersection is at less than 75% size and tab is active', function * () { windowStore.state = defaultWindowStore - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at75) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 1) }) }) describe('should not show icon', function () { - it('if tab is pinned', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: true, - pinnedLocation: true - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') - }) - - it('if tab size is largeMedium and tab is not active', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - hoverState: true, - breakpoint: 'largeMedium' - }] - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') - }) - - it('if tab size is medium and tab is not active', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - hoverState: true, - breakpoint: 'medium' - }] - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') - }) - - it('if tab size is mediumSmall and tab is not active', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - hoverState: true, - breakpoint: 'mediumSmall' - }] - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') - }) - - it('if tab size is small and tab is not active', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - hoverState: true, - breakpoint: 'small' - }] - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') + it('if tab is intersected at 20% size or less', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'intersectionRatio', intersection.at20]) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if tab size is extraSmall and tab is not active', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - hoverState: true, - breakpoint: 'extraSmall' - }] + it('if not intersected and tab is not hovered', function * () { + windowStore.state = defaultWindowStore.mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection, + hoverTabIndex: 1337 }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - // TODO check what is going on - it.skip('if tab size is the smallest size', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - hoverState: true, - breakpoint: 'extraSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props()['data-test2-id'], 'close-icon-off') + it('if intersection is at less than 75% size and tab is not active', function * () { + windowStore.state = defaultWindowStore + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) }) }) diff --git a/test/unit/app/renderer/components/tabs/content/favIconTest.js b/test/unit/app/renderer/components/tabs/content/favIconTest.js index 8dcce4144ac..75d62d9603f 100644 --- a/test/unit/app/renderer/components/tabs/content/favIconTest.js +++ b/test/unit/app/renderer/components/tabs/content/favIconTest.js @@ -5,34 +5,32 @@ const mockery = require('mockery') const {mount} = require('enzyme') -const Immutable = require('immutable') const assert = require('assert') -const globalStyles = require('../../../../../../../app/renderer/components/styles/global') +const Immutable = require('immutable') const fakeElectron = require('../../../../../lib/fakeElectron') require('../../../../../braveUnit') -const url1 = 'https://brave.com' -const favicon1 = 'https://brave.com/favicon.ico' - +const index = 0 const tabId = 1 const frameKey = 1 -const fakeAppStoreRenderer = { - state: Immutable.fromJS({ - windows: [{ - windowId: 1, - windowUUID: 'uuid' - }], - tabs: [{ - tabId: tabId, - windowId: 1, - windowUUID: 'uuid', - url: 'https://brave.com' - }] - }), - addChangeListener: () => {}, - removeChangeListener: () => {} -} +const fakeAppStoreRenderer = Immutable.fromJS({ + windows: [{ + windowId: 1, + windowUUID: 'uuid' + }], + tabs: [{ + tabId: tabId, + windowId: 1, + windowUUID: 'uuid', + url: 'https://brave.com' + }], + tabsInternal: { + index: { + 1: 0 + } + } +}) const defaultWindowStore = Immutable.fromJS({ activeFrameKey: frameKey, @@ -42,7 +40,8 @@ const defaultWindowStore = Immutable.fromJS({ location: 'http://brave.com' }], tabs: [{ - key: frameKey + key: frameKey, + index: index }], framesInternal: { index: { @@ -55,7 +54,7 @@ const defaultWindowStore = Immutable.fromJS({ }) describe('Tabs content - Favicon', function () { - let Tab, windowStore + let Favicon, windowStore, appStore before(function () { mockery.enable({ @@ -64,17 +63,12 @@ describe('Tabs content - Favicon', function () { useCleanCache: true }) mockery.registerMock('electron', fakeElectron) - mockery.registerMock('../../../js/l10n', { - translation: () => 'translated' - }) - mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) + mockery.registerMock('../../../../extensions/brave/img/tabs/default.svg') mockery.registerMock('../../../../extensions/brave/img/tabs/loading.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/new_session.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/private.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_hover.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_normal.svg') windowStore = require('../../../../../../../js/stores/windowStore') - Tab = require('../../../../../../../app/renderer/components/tabs/tab') + appStore = require('../../../../../../../js/stores/appStoreRenderer') + Favicon = require('../../../../../../../app/renderer/components/tabs/content/favicon') + appStore.state = fakeAppStoreRenderer }) after(function () { @@ -82,43 +76,71 @@ describe('Tabs content - Favicon', function () { mockery.disable() }) - describe('should show', function () { - it('favicon if page has one', function () { + describe('loading icon', function () { + it('shows when tab is loading', function * () { + windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { + icon: 'winter-is-coming.jpg', + loading: true + }) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], 'loading') + }) + it('does not show when tab is not loading', function * () { windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - icon: favicon1, + icon: 'winter-is-coming.jpg', loading: false }) - const wrapper = mount() - assert.equal(wrapper.find('Favicon').length, 1) + const wrapper = mount() + assert.notEqual(wrapper.find('TabIcon').props()['data-test-id'], 'loading') + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], 'winter-is-coming.jpg') }) - - it('placeholder icon if page has no favicon', function () { + }) + describe('default icon', function () { + it('shows when tab has no icon', function * () { windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, icon: null, loading: false }) - const wrapper = mount() - assert.equal(wrapper.find('Favicon TabIcon').props().symbol, globalStyles.appIcons.defaultIcon) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], 'defaultIcon') + }) + it('does not show when tab has an icon', function * () { + windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { + icon: 'the-night-is-dark-and-full-of-terror.jpg', + loading: false + }) + const wrapper = mount() + assert.notEqual(wrapper.find('TabIcon').props()['data-test-id'], 'defaultIcon') + }) + }) + describe('favicon', function () { + it('shows if page has a favicon', function * () { + windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { + icon: 'bbondy-king-of-the-north.jpg', + loading: false + }) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], 'bbondy-king-of-the-north.jpg') }) - it('loading icon if page is still loading', function () { + it('does not show if page is loading', function * () { windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - icon: favicon1, + icon: 'iron-throne-belongs-to-serg.jpg', loading: true }) - const wrapper = mount() - assert.equal(wrapper.find('Favicon TabIcon').props()['data-test-id'], 'loading') + const wrapper = mount() + assert.notEqual(wrapper.find('TabIcon').props()['data-test-id'], 'iron-throne-belongs-to-serg.jpg') + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], 'loading') }) - }) - it('should not show favicon for new tab page', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: 'about:newtab' + it('does not show if page has no favicon', function * () { + windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { + icon: null, + loading: false + }) + const wrapper = mount() + assert.notEqual(wrapper.find('TabIcon').props()['data-test-id'], null) + assert.equal(wrapper.find('TabIcon').props()['data-test-id'], 'defaultIcon') }) - const wrapper = mount() - assert.equal(wrapper.find('Favicon').length, 0) }) }) diff --git a/test/unit/app/renderer/components/tabs/content/newSessionIconTest.js b/test/unit/app/renderer/components/tabs/content/newSessionIconTest.js index e0aac5db86f..24470da8fdb 100644 --- a/test/unit/app/renderer/components/tabs/content/newSessionIconTest.js +++ b/test/unit/app/renderer/components/tabs/content/newSessionIconTest.js @@ -7,38 +7,39 @@ const mockery = require('mockery') const {mount} = require('enzyme') const assert = require('assert') const Immutable = require('immutable') -const {tabs} = require('../../../../../../../js/constants/config') const fakeElectron = require('../../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../../app/renderer/components/styles/global') require('../../../../../braveUnit') const index = 0 const tabId = 1 const frameKey = 1 -const invalidFrameKey = 71 -const fakeAppStoreRenderer = { - state: Immutable.fromJS({ - windows: [{ - windowId: 1, - windowUUID: 'uuid' - }], - tabs: [{ - tabId: tabId, - windowId: 1, - windowUUID: 'uuid', - url: 'https://brave.com' - }] - }), - addChangeListener: () => {}, - removeChangeListener: () => {} -} +const fakeAppStoreRenderer = Immutable.fromJS({ + windows: [{ + windowId: 1, + windowUUID: 'uuid' + }], + tabs: [{ + tabId: tabId, + windowId: 1, + windowUUID: 'uuid', + url: 'https://brave.com' + }], + tabsInternal: { + index: { + 1: 0 + } + } +}) const defaultWindowStore = Immutable.fromJS({ activeFrameKey: frameKey, frames: [{ key: frameKey, tabId: tabId, - location: 'http://brave.com' + location: 'http://brave.com', + partitionNumber: 1 }], tabs: [{ key: frameKey, @@ -54,13 +55,13 @@ const defaultWindowStore = Immutable.fromJS({ }, ui: { tabs: { - hoverTabIndex: index + tabHoverState: 1 } } }) describe('Tabs content - NewSessionIcon', function () { - let Tab, windowStore, NewSessionIcon + let NewSessionIcon, windowStore, appStore before(function () { mockery.enable({ @@ -69,15 +70,11 @@ describe('Tabs content - NewSessionIcon', function () { useCleanCache: true }) mockery.registerMock('electron', fakeElectron) - mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) - mockery.registerMock('../../../../extensions/brave/img/tabs/loading.svg') mockery.registerMock('../../../../extensions/brave/img/tabs/new_session.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/private.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_hover.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_normal.svg') windowStore = require('../../../../../../../js/stores/windowStore') - Tab = require('../../../../../../../app/renderer/components/tabs/tab') - NewSessionIcon = require('../../../../../../../app/renderer/components/tabs/content/newSessionIcon') + appStore = require('../../../../../../../js/stores/appStoreRenderer') + NewSessionIcon = require('../../../../../../../app/renderer/components/tabs/content/NewSessionIcon') + appStore.state = fakeAppStoreRenderer }) after(function () { @@ -85,195 +82,63 @@ describe('Tabs content - NewSessionIcon', function () { mockery.disable() }) - describe('should show', function () { - it('icon if current tab is a new session tab', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - partitionNumber: 1, - breakpoint: 'default' - }], - ui: { - tabs: { - hoverTabIndex: null - } - } - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 1) - }) - - it('icon if mouse is not over tab and breakpoint is default', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - partitionNumber: 1, - breakpoint: 'default' - }], - ui: { - tabs: { - hoverTabIndex: null - } - } - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 1) - }) - - it('icon if mouse is not over tab and breakpoint is large', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - partitionNumber: 1, - breakpoint: 'large' - }], - ui: { - tabs: { - hoverTabIndex: null - } - } - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 1) - }) - - it('icon if tab is not active and breakpoint is largeMedium', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - partitionNumber: 1, - breakpoint: 'largeMedium' - }], - ui: { - tabs: { - hoverTabIndex: null - } - } - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 1) - }) - - it('partition number for new sessions', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 3, - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props().symbolContent, 3) - }) - - it('partition number for sessions with number set by opener (ex: clicking target=_blank)', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 'partition-3', - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props().symbolContent, 3) + describe('should show icon', function () { + it('if tab is not hovered', function * () { + windowStore.state = defaultWindowStore + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection, + tabHoverIndex: 1337 + }) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 1) }) - it('max partition number even if session is bigger', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1000, - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabIcon').props().symbolContent, tabs.maxAllowedNewSessions) - }) - it('passing in a frame key which does not exist does not fail', function () { + it('if tab is not active and size is small', function * () { windowStore.state = defaultWindowStore - const wrapper = mount() - assert(wrapper.find('TabIcon')) + .mergeIn(['ui', 'tabs'], { + tabHoverIndex: 1337, + intersectionRatio: intersection.at45 + }) + .set('activeFrameKey', 1337) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 1) }) }) describe('should not show icon', function () { - it('if current tab is not private', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: false - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) - }) - - it('if mouse is over tab and breakpoint is default', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: true, - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) - }) - - it('if mouse is over tab and breakpoint is large', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: true, - breakpoint: 'large' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) - }) - - it('if tab is active and breakpoint is largeMedium', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: true, - breakpoint: 'largeMedium' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) - }) - - it('if breakpoint is medium', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: false, - breakpoint: 'medium' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) + it('if tab is not partitioned', function * () { + windowStore.state = defaultWindowStore + .setIn(['frames', index, 'partitionNumber'], false) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if breakpoint is mediumSmall', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: false, - breakpoint: 'mediumSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) + it('if tab is being hovered', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'hoverTabIndex'], index) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if breakpoint is small', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: true, - breakpoint: 'small' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) + it('if for active tab if size is small', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if breakpoint is extraSmall', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: true, - breakpoint: 'extraSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) + it('if tab is being intersected at 35% or less', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at20) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if breakpoint is the smallest', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - partitionNumber: 1, - hoverState: true, - breakpoint: 'smallest' - }) - const wrapper = mount() - assert.equal(wrapper.find('NewSessionIcon').length, 0) + it('if partitionNumber is zero', function * () { + windowStore.state = defaultWindowStore + .setIn(['frames', index, 'partitionNumber'], 0) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) }) }) diff --git a/test/unit/app/renderer/components/tabs/content/privateIconTest.js b/test/unit/app/renderer/components/tabs/content/privateIconTest.js index c8c95cfeca4..88dc8d5dbaf 100644 --- a/test/unit/app/renderer/components/tabs/content/privateIconTest.js +++ b/test/unit/app/renderer/components/tabs/content/privateIconTest.js @@ -5,42 +5,45 @@ const mockery = require('mockery') const {mount} = require('enzyme') -const Immutable = require('immutable') const assert = require('assert') +const Immutable = require('immutable') const fakeElectron = require('../../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../../app/renderer/components/styles/global') require('../../../../../braveUnit') const index = 0 const tabId = 1 const frameKey = 1 -const fakeAppStoreRenderer = { - state: Immutable.fromJS({ - windows: [{ - windowId: 1, - windowUUID: 'uuid' - }], - tabs: [{ - tabId: tabId, - windowId: 1, - windowUUID: 'uuid', - url: 'https://brave.com' - }] - }), - addChangeListener: () => {}, - removeChangeListener: () => {} -} +const fakeAppStoreRenderer = Immutable.fromJS({ + windows: [{ + windowId: 1, + windowUUID: 'uuid' + }], + tabs: [{ + tabId: tabId, + windowId: 1, + windowUUID: 'uuid', + url: 'https://brave.com' + }], + tabsInternal: { + index: { + 1: 0 + } + } +}) const defaultWindowStore = Immutable.fromJS({ activeFrameKey: frameKey, frames: [{ key: frameKey, tabId: tabId, - location: 'http://brave.com' + location: 'http://brave.com', + isPrivate: true }], tabs: [{ - index: index, - key: frameKey + key: frameKey, + index: index }], framesInternal: { index: { @@ -52,13 +55,13 @@ const defaultWindowStore = Immutable.fromJS({ }, ui: { tabs: { - hoverTabIndex: index + tabHoverState: 1 } } }) describe('Tabs content - PrivateIcon', function () { - let Tab, windowStore + let PrivateIcon, windowStore, appStore before(function () { mockery.enable({ @@ -67,14 +70,11 @@ describe('Tabs content - PrivateIcon', function () { useCleanCache: true }) mockery.registerMock('electron', fakeElectron) - mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) - mockery.registerMock('../../../../extensions/brave/img/tabs/loading.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/new_session.svg') mockery.registerMock('../../../../extensions/brave/img/tabs/private.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_hover.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_normal.svg') windowStore = require('../../../../../../../js/stores/windowStore') - Tab = require('../../../../../../../app/renderer/components/tabs/tab') + appStore = require('../../../../../../../js/stores/appStoreRenderer') + PrivateIcon = require('../../../../../../../app/renderer/components/tabs/content/privateIcon') + appStore.state = fakeAppStoreRenderer }) after(function () { @@ -83,158 +83,55 @@ describe('Tabs content - PrivateIcon', function () { }) describe('should show icon', function () { - it('if current tab is private', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - isPrivate: true, - breakpoint: 'default' - }], - ui: { - tabs: { - hoverTabIndex: null - } - } - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 1) - }) - - it('if tab is not active and breakpoint is largeMedium', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - isPrivate: true, - hoverState: false, - breakpoint: 'largeMedium' - }] - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 1) - }) - - it('if mouse is not over tab and breakpoint is large', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - isPrivate: true, - breakpoint: 'large' - }], - ui: { - tabs: { - hoverTabIndex: null - } - } - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 1) + it('if tab is not hovered', function * () { + windowStore.state = defaultWindowStore + .mergeIn(['ui', 'tabs'], { + intersectionRatio: intersection.noIntersection, + tabHoverIndex: 1337 + }) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 1) }) - it('if mouse is not over tab and breakpoint is default', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - isPrivate: true, - breakpoint: 'default' - }], - ui: { - tabs: { - hoverTabIndex: null - } - } - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 1) + it('if tab is not active and size is small', function * () { + windowStore.state = defaultWindowStore + .mergeIn(['ui', 'tabs'], { + tabHoverIndex: 1337, + intersectionRatio: intersection.at45 + }) + .set('activeFrameKey', 1337) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 1) }) }) describe('should not show icon', function () { - it('if current tab is not private', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: false - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) - }) - - it('if mouse is over tab and breakpoint is default', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: true, - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) - }) - - it('if mouse is over tab and breakpoint is large', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: true, - breakpoint: 'large' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) - }) - - it('if tab is active and breakpoint is largeMedium', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: true, - breakpoint: 'largeMedium' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) - }) - - it('if breakpoint is medium', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: false, - breakpoint: 'medium' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) - }) - - it('if breakpoint is mediumSmall', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: false, - breakpoint: 'mediumSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) + it('if tab is not private', function * () { + windowStore.state = defaultWindowStore + .setIn(['frames', index, 'isPrivate'], false) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if breakpoint is small', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: true, - breakpoint: 'small' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) + it('if tab is being hovered', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'hoverTabIndex'], index) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if breakpoint is extraSmall', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: true, - breakpoint: 'extraSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) + it('if for active tab if size is small', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) - it('if breakpoint is the smallest', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - isPrivate: true, - hoverState: true, - breakpoint: 'smallest' - }) - const wrapper = mount() - assert.equal(wrapper.find('PrivateIcon').length, 0) + it('if tab is being intersected at 35% or less', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at20) + const wrapper = mount() + assert.equal(wrapper.find('TabIcon').length, 0) }) }) }) diff --git a/test/unit/app/renderer/components/tabs/content/tabTitleTest.js b/test/unit/app/renderer/components/tabs/content/tabTitleTest.js index f0924afab5c..c144e43fbab 100644 --- a/test/unit/app/renderer/components/tabs/content/tabTitleTest.js +++ b/test/unit/app/renderer/components/tabs/content/tabTitleTest.js @@ -1,49 +1,48 @@ /* 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/. */ - -/* global describe, before, it, after */ +/* global describe, before, after, it */ const mockery = require('mockery') const {mount} = require('enzyme') const assert = require('assert') const Immutable = require('immutable') const fakeElectron = require('../../../../../lib/fakeElectron') +const {intersection} = require('../../../../../../../app/renderer/components/styles/global') require('../../../../../braveUnit') -const url1 = 'https://brave.com' -const pageTitle1 = 'Brave Software' +const index = 0 const tabId = 1 const frameKey = 1 -const invalidFrameKey = 71 -const fakeAppStoreRenderer = { - state: Immutable.fromJS({ - windows: [{ - windowId: 1, - windowUUID: 'uuid' - }], - tabs: [{ - tabId: tabId, - windowId: 1, - windowUUID: 'uuid', - url: url1 - }] - }), - addChangeListener: () => {}, - removeChangeListener: () => {} -} +const fakeAppStoreRenderer = Immutable.fromJS({ + windows: [{ + windowId: 1, + windowUUID: 'uuid' + }], + tabs: [{ + tabId: tabId, + windowId: 1, + windowUUID: 'uuid', + url: 'https://brave.com' + }], + tabsInternal: { + index: { + 1: 0 + } + } +}) const defaultWindowStore = Immutable.fromJS({ activeFrameKey: frameKey, frames: [{ key: frameKey, tabId: tabId, - location: url1, - title: pageTitle1 + location: 'http://brave.com' }], tabs: [{ - key: frameKey + key: frameKey, + index: index }], framesInternal: { index: { @@ -52,12 +51,16 @@ const defaultWindowStore = Immutable.fromJS({ tabIndex: { 1: 0 } + }, + ui: { + tabs: { + tabHoverState: 1 + } } }) -describe('Tabs content - Title', function () { - let Tab, windowStore - +describe('Tabs content - TabTitle', function () { + let TabTitle, windowStore, appStore before(function () { mockery.enable({ warnOnReplace: false, @@ -65,14 +68,12 @@ describe('Tabs content - Title', function () { useCleanCache: true }) mockery.registerMock('electron', fakeElectron) - mockery.registerMock('../../../js/stores/appStoreRenderer', fakeAppStoreRenderer) - mockery.registerMock('../../../../extensions/brave/img/tabs/loading.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/new_session.svg') + mockery.registerMock('../../../../js/l10n', { translation: () => 'translated' }) mockery.registerMock('../../../../extensions/brave/img/tabs/private.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_hover.svg') - mockery.registerMock('../../../../extensions/brave/img/tabs/close_btn_normal.svg') windowStore = require('../../../../../../../js/stores/windowStore') - Tab = require('../../../../../../../app/renderer/components/tabs/tab') + appStore = require('../../../../../../../js/stores/appStoreRenderer') + TabTitle = require('../../../../../../../app/renderer/components/tabs/content/tabTitle') + appStore.state = fakeAppStoreRenderer }) after(function () { @@ -80,118 +81,86 @@ describe('Tabs content - Title', function () { mockery.disable() }) - describe('should show text', function () { - it('if page has a title', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1 - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle div').text(), pageTitle1) - }) - it('if breakpoint is default', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - breakpoint: 'default' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle div').text(), pageTitle1) - }) - it('if breakpoint is large', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - breakpoint: 'large' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle div').text(), pageTitle1) + describe('should show icon', function () { + it('if is not intersected at 35% of tab size', function * () { + windowStore.state = defaultWindowStore + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at60) + .mergeIn(['frames', index], { + isPrivate: false, + partitionNumber: false + }) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, true) }) - it('if breakpoint is medium', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - breakpoint: 'medium' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle div').text(), pageTitle1) + + it('if not active and intersected at 45% of tab size with no private icon visible', function * () { + windowStore.state = defaultWindowStore + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + .setIn(['frames', index, 'isPrivate'], false) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, true) }) - it('if breakpoint is mediumSmall and tab is not active', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - location: url1, - title: pageTitle1, - breakpoint: 'mediumSmall' - }] - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle div').text(), pageTitle1) + + it('if not active and intersected at 45% of tab size with no partition icon visible', function * () { + windowStore.state = defaultWindowStore + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + .setIn(['frames', index, 'partitionNumber'], null) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, true) }) - it('if breakpoint is small and tab is not active', function () { - windowStore.state = defaultWindowStore.merge({ - activeFrameKey: 0, - frames: [{ - location: url1, - title: pageTitle1, - breakpoint: 'small' - }] - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle div').text(), pageTitle1) + + it('if is intersected at 45% of tab size and is about:newtab', function * () { + windowStore.state = defaultWindowStore + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + .setIn(['frames', index, 'location'], 'about:newtab') + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, true) }) - it('passing in a frame key which does not exist does not fail', function () { + + it('if is intersected at 45% of tab size and is not active', function * () { windowStore.state = defaultWindowStore - const wrapper = mount() - assert.equal(wrapper.find('TabTitle div').text(), '') + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, true) }) }) - describe('should not show text', function () { - it('if tab is pinned', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - pinnedLocation: true - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle').length, 0) - }) - it('if breakpoint is mediumSmall and tab is active', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - breakpoint: 'mediumSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle').length, 0) + describe('should not show icon', function () { + it('if is intersected at 35% of tab size', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at40) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, false) }) - it('if breakpoint is small and tab is active', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - breakpoint: 'small' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle').length, 0) + + it('if active and intersected at 45% of tab size', function * () { + windowStore.state = defaultWindowStore + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, false) }) - it('if breakpoint is extraSmall', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - breakpoint: 'extraSmall' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle').length, 0) + + it('if not active and intersected at 45% of tab size with partition icon visible', function * () { + windowStore.state = defaultWindowStore + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + .setIn(['frames', index, 'partitionNumber'], 1) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, false) }) - it('if breakpoint is the smallest', function () { - windowStore.state = defaultWindowStore.mergeIn(['frames', 0], { - location: url1, - title: pageTitle1, - breakpoint: 'smallest' - }) - const wrapper = mount() - assert.equal(wrapper.find('TabTitle').length, 0) + + it('if not active and intersected at 45% of tab size with private icon visible', function * () { + windowStore.state = defaultWindowStore + .set('activeFrameKey', 1337) + .setIn(['ui', 'tabs', 'intersectionRatio'], intersection.at45) + .setIn(['frames', index, 'isPrivate'], true) + const wrapper = mount() + assert.equal(wrapper.find('TabTitle').props().showTabTitle, false) }) }) }) diff --git a/test/unit/js/stores/windowStoreTest.js b/test/unit/js/stores/windowStoreTest.js index 6f8e5a294e1..7538230b7e5 100644 --- a/test/unit/js/stores/windowStoreTest.js +++ b/test/unit/js/stores/windowStoreTest.js @@ -169,7 +169,6 @@ describe('Window store unit tests', function () { }, tabId: 8, zoomLevel: 0, - breakpoint: 'default', index: 1, partitionNumber: 0, history: [], diff --git a/test/unit/state/frameStateUtilTest.js b/test/unit/state/frameStateUtilTest.js index 4752268dd8c..902e13a85fe 100644 --- a/test/unit/state/frameStateUtilTest.js +++ b/test/unit/state/frameStateUtilTest.js @@ -411,4 +411,37 @@ describe('frameStateUtil', function () { assert.equal(result, 1) }) }) + + describe('frameLocationMatch', function () { + before(function () { + this.frameKey = 1 + this.location = 'nespresso.com' + this.state = defaultWindowStore.mergeIn(['frames', 0], { + location: this.location, + frameKey: this.frameKey + }) + this.frame = this.state.getIn(['frames', 0]) + }) + + it('returns false if frame is empty', function () { + const result = frameStateUtil.frameLocationMatch(null, this.location) + assert.equal(result, false) + }) + it('returns false if frame is not an Immutable map', function () { + const result = frameStateUtil.frameLocationMatch(this.frame.toJS(), this.location) + assert.equal(result, false) + }) + it('returns false if location is empty', function () { + const result = frameStateUtil.frameLocationMatch(this.frame, '') + assert.equal(result, false) + }) + it('returns false if location is a partial match', function () { + const result = frameStateUtil.frameLocationMatch(this.frame, 'nespresso.co.uk') + assert.equal(result, false) + }) + it('returns true if location match', function () { + const result = frameStateUtil.frameLocationMatch(this.frame, this.location) + assert.equal(result, true) + }) + }) })