diff --git a/app/common/lib/platformUtil.js b/app/common/lib/platformUtil.js index 0388203e24f..7a79b1a89a3 100644 --- a/app/common/lib/platformUtil.js +++ b/app/common/lib/platformUtil.js @@ -42,3 +42,8 @@ module.exports.isWindows = () => { return process.platform === 'win32' || navigator.platform === 'Win32' } + +module.exports.isLinux = () => { + return !module.exports.isDarwin() && + !module.exports.isWindows() +} diff --git a/app/renderer/components/styles/global.js b/app/renderer/components/styles/global.js index 41704a33598..2f0e53bfeeb 100644 --- a/app/renderer/components/styles/global.js +++ b/app/renderer/components/styles/global.js @@ -4,7 +4,15 @@ const globalStyles = { breakpointNarrowViewport: '600px', breakpointExtensionButtonPadding: '720px', breakpointSmallWin32: '650px', - breakpointTinyWin32: '500px' + breakpointTinyWin32: '500px', + tab: { + largeMedium: '83px', + medium: '66px', + mediumSmall: '53px', + small: '42px', + extraSmall: '33px', + smallest: '19px' + } }, color: { linkColor: '#0099CC', @@ -95,7 +103,9 @@ const globalStyles = { navbarBraveButtonMarginLeft: '80px', navbarLeftMarginDarwin: '76px', sideBarWidth: '190px', - aboutPageSectionPadding: '24px' + aboutPageSectionPadding: '24px', + defaultTabPadding: '0 4px', + defaultIconPadding: '0 2px' }, shadow: { switchShadow: 'inset 0 1px 4px rgba(0, 0, 0, 0.35)', @@ -136,6 +146,19 @@ const globalStyles = { zindexSuggestionText: '3100', zindexWindowFullScreen: '4000', zindexWindowFullScreenBanner: '4100' + }, + fontSize: { + tabIcon: '14px', + tabTitle: '12px' + }, + appIcons: { + loading: 'fa fa-spinner fa-spin', + defaultIcon: 'fa fa-file-o', + closeTab: 'fa fa-times-circle', + private: 'fa fa-eye', + newSession: 'fa fa-user', + volumeOn: 'fa fa-volume-up', + volumeOff: 'fa fa-volume-off' } } diff --git a/app/renderer/components/styles/tab.js b/app/renderer/components/styles/tab.js new file mode 100644 index 00000000000..c01c601442f --- /dev/null +++ b/app/renderer/components/styles/tab.js @@ -0,0 +1,126 @@ +/* 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 {StyleSheet} = require('aphrodite') +const globalStyles = require('./global') + +const styles = StyleSheet.create({ + // Windows specific style + tabForWindows: { + color: '#555' + }, + + tab: { + background: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1))', + borderRadius: `${globalStyles.radius.borderRadiusTabs} ${globalStyles.radius.borderRadiusTabs} 0 0`, + borderWidth: '1px 1px 0', + borderStyle: 'solid', + borderColor: 'transparent', + boxSizing: 'border-box', + color: '#3B3B3B', + display: 'flex', + height: '23px', + marginTop: '2px', + transition: 'transform 200ms ease', + left: '0', + opacity: '1', + width: '100%', + alignItems: 'center', + justifyContent: 'space-between', + padding: globalStyles.spacing.defaultTabPadding, + position: 'relative', + + ':hover': { + background: 'linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(250, 250, 250, 0.4))' + } + }, + + // Custom classes based on tab's width and behaviour + + tabNarrowView: { + padding: '0 2px' + }, + + narrowViewPlayIndicator: { + borderWidth: '2px 0 0', + borderStyle: 'solid', + borderColor: 'lightskyblue' + }, + + tabNarrowestView: { + justifyContent: 'center' + }, + + tabMinAllowedSize: { + padding: 0 + }, + + tabIdNarrowView: { + flex: 'inherit' + }, + + tabIdMinAllowedSize: { + overflow: 'hidden' + }, + + // Add extra space for pages that have no icon + // such as about:blank and about:newtab + noFavicon: { + padding: '0 6px' + }, + + alternativePlayIndicator: { + borderTop: '2px solid lightskyblue' + }, + + tabId: { + alignItems: 'center', + display: 'flex', + flex: '1', + minWidth: '0' // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1108514#c5 + }, + + isPinned: { + padding: globalStyles.spacing.defaultIconPadding + }, + + active: { + background: `linear-gradient(to bottom, #fff, ${globalStyles.color.chromePrimary})`, + height: '25px', + marginTop: '1px', + boxShadow: '0 -1px 4px 0 rgba(51, 51, 51, 0.12)', + borderWidth: '1px 1px 0', + borderStyle: 'solid', + borderColor: '#bbb', + + ':hover': { + background: `linear-gradient(to bottom, #fff, ${globalStyles.color.chromePrimary})` + } + }, + + activePrivateTab: { + background: 'rgb(247, 247, 247)', + color: 'black' + }, + + private: { + background: '#9c8dc1', // (globalStyles.color.privateTabBackground, 40%) + color: '#fff', + + ':hover': { + background: '#665296', // (globalStyles.color.privateTabBackground, 20%) + color: '#fff' + } + }, + + dragging: { + ':hover': { + closeTab: { + opacity: '0' + } + } + } +}) + +module.exports = styles diff --git a/app/renderer/components/tabContent.js b/app/renderer/components/tabContent.js new file mode 100644 index 00000000000..69ba87e6662 --- /dev/null +++ b/app/renderer/components/tabContent.js @@ -0,0 +1,268 @@ +/* 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 React = require('react') +const ImmutableComponent = require('../../../js/components/immutableComponent') +const {StyleSheet, css} = require('aphrodite/no-important') +const globalStyles = require('./styles/global') +const {isWindows, isLinux} = require('../../common/lib/platformUtil') + +/** + * Boilerplate component for all tab icons + */ +class TabIcon extends ImmutableComponent { + render () { + const tabIconStyle = { + // Currently it's not possible to concatenate Aphrodite generated classes + // and pre-built classes using default Aphrodite API, so we keep with inline-style + fontSize: 'inherit', + display: 'flex', + alignSelf: 'center', + width: '16px', + height: '16px', + alignItems: 'center', + justifyContent: 'center' + } + return
+ { + this.props.symbol + ? + : null + } +
+ } +} + +class Favicon extends ImmutableComponent { + get favicon () { + return !this.props.isLoading && this.props.tabProps.get('icon') + } + + get loadingIcon () { + return this.props.isLoading + ? globalStyles.appIcons.loading + : null + } + + get defaultIcon () { + return (!this.props.isLoading && !this.favicon) + ? globalStyles.appIcons.defaultIcon + : null + } + + render () { + const iconStyles = StyleSheet.create({ + favicon: {backgroundImage: `url(${this.favicon})`} + }) + return this.props.tabProps.get('location') !== 'about:newtab' + ? + : null + } +} + +class AudioTabIcon extends ImmutableComponent { + get pageCanPlayAudio () { + return this.props.tabProps.get('audioPlaybackActive') || this.props.tabProps.get('audioMuted') + } + + get narrowView () { + const sizes = ['medium', 'mediumSmall', 'small', 'extraSmall', 'smallest'] + return sizes.includes(this.props.tabProps.get('breakpoint')) + } + + get locationHasSecondaryIcon () { + return !!this.props.tabProps.get('isPrivate') || !!this.props.tabProps.get('partitionNumber') + } + + get mutedState () { + return this.pageCanPlayAudio && this.props.tabProps.get('audioMuted') + } + + get unmutedState () { + this.props.tabProps.get('audioPlaybackActive') && !this.props.tabProps.get('audioMuted') + } + + get audioIcon () { + return !this.mutedState + ? globalStyles.appIcons.volumeOn + : globalStyles.appIcons.volumeOff + } + + render () { + return this.pageCanPlayAudio && !this.narrowView + ? + : null + } +} + +class PrivateIcon extends ImmutableComponent { + get narrowView () { + const sizes = ['small', 'extraSmall', 'smallest'] + return sizes.includes(this.props.tabProps.get('breakpoint')) + } + + render () { + return this.props.tabProps.get('isPrivate') && !this.props.tabProps.get('hoverState') && !this.narrowView + ? + : null + } +} + +class NewSessionIcon extends ImmutableComponent { + get narrowView () { + const sizes = ['small', 'extraSmall', 'smallest'] + return sizes.includes(this.props.tabProps.get('breakpoint')) + } + + render () { + return this.props.tabProps.get('partitionNumber') && !this.props.tabProps.get('hoverState') && !this.narrowView + ? + : null + } +} + +class TabTitle extends ImmutableComponent { + get locationHasSecondaryIcon () { + return !!this.props.tabProps.get('isPrivate') || !!this.props.tabProps.get('partitionNumber') + } + + get isPinned () { + return !!this.props.tabProps.get('pinnedLocation') + } + + get pageCanPlayAudio () { + return this.props.tabProps.get('audioPlaybackActive') || this.props.tabProps.get('audioMuted') + } + + get shouldHideTitle () { + return (this.props.tabProps.get('breakpoint') === 'largeMedium' && this.pageCanPlayAudio && this.locationHasSecondaryIcon) || + (this.props.tabProps.get('breakpoint') === 'mediumSmall' && this.locationHasSecondaryIcon) || + this.props.tabProps.get('breakpoint') === 'extraSmall' || this.props.tabProps.get('breakpoint') === 'smallest' + } + + render () { + return !this.isPinned && !this.shouldHideTitle + ?
+ {this.props.pageTitle} +
+ : null + } +} + +class CloseTabIcon extends ImmutableComponent { + get isPinned () { + return !!this.props.tabProps.get('pinnedLocation') + } + + get narrowView () { + const sizes = ['extraSmall', 'smallest'] + return sizes.includes(this.props.tabProps.get('breakpoint')) + } + + render () { + return this.props.tabProps.get('hoverState') && !this.narrowView && !this.isPinned + ? + : null + } +} + +const styles = StyleSheet.create({ + icon: { + width: '16px', + minWidth: '16px', + height: '16px', + backgroundSize: '16px', + fontSize: globalStyles.fontSize.tabIcon, + backgroundPosition: 'center', + backgroundRepeat: 'no-repeat', + display: 'flex', + alignSelf: 'center', + position: 'relative', + textAlign: 'center', + justifyContent: 'center', + padding: globalStyles.spacing.defaultIconPadding + }, + + iconNarrowView: { + padding: 0 + }, + + audioIcon: { + color: globalStyles.color.highlightBlue + }, + + closeTab: { + opacity: '0.7', + position: 'absolute', + top: '0', + right: '0', + padding: '0 4px', + borderTopRightRadius: globalStyles.radius.borderRadius, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + fontSize: '16px', + width: '16px', + height: '100%', + border: '0', + zIndex: globalStyles.zindex.zindexTabs, + + ':hover': { + opacity: '1' + } + }, + + tabTitle: { + WebkitUserSelect: 'none', + boxSizing: 'border-box', + fontSize: globalStyles.fontSize.tabTitle, + overflow: 'hidden', + textOverflow: 'ellipsis', + whiteSpace: 'nowrap', + height: '15px', + padding: globalStyles.spacing.defaultTabPadding + }, + + tabTitleForWindows: { + fontWeight: '500', + fontSize: globalStyles.fontSize.tabTitle, + height: '18px' + }, + + tabTitleForLinux: { + height: globalStyles.fontSize.tabTitle + } +}) + +module.exports = { + Favicon, + AudioTabIcon, + NewSessionIcon, + PrivateIcon, + TabTitle, + CloseTabIcon +} diff --git a/app/renderer/components/tabIcon.js b/app/renderer/components/tabIcon.js deleted file mode 100644 index 00d6b5174d7..00000000000 --- a/app/renderer/components/tabIcon.js +++ /dev/null @@ -1,47 +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/. */ - -const React = require('react') -const ImmutableComponent = require('../../../js/components/immutableComponent') -const {StyleSheet, css} = require('aphrodite') -const globalStyles = require('./styles/global') - -class TabIcon extends ImmutableComponent { - render () { - const className = css( - styles.icon, - this.props.withBlueIcon && styles.blueIcon - ) - return
- -
- } -} - -class AudioTabIcon extends ImmutableComponent { - render () { - return - } -} - -const styles = StyleSheet.create({ - 'icon': { - backgroundPosition: 'center', - backgroundRepeat: 'no-repeat', - display: 'inline-block', - fontSize: '14px', - margin: 'auto 7px auto 7px', - position: 'relative', - verticalAlign: 'middle', - textAlign: 'center' - }, - 'blueIcon': { - color: globalStyles.color.highlightBlue - } -}) - -module.exports = { - TabIcon, - AudioTabIcon -} diff --git a/app/renderer/lib/tabUtil.js b/app/renderer/lib/tabUtil.js new file mode 100644 index 00000000000..52a80f25ce1 --- /dev/null +++ b/app/renderer/lib/tabUtil.js @@ -0,0 +1,27 @@ +/* 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 styles = require('../components/styles/global') + +/** + * Get tab's breakpoint name for current tab size. + * @param {Number} The current tab size + * @returns {String} The matching breakpoint. + */ +module.exports.getTabBreakpoint = (tabWidth) => { + const sizes = ['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 diff --git a/docs/state.md b/docs/state.md index 96404cdaeb9..384121fe7ce 100644 --- a/docs/state.md +++ b/docs/state.md @@ -567,8 +567,10 @@ WindowStore tabs: [{ audioMuted: boolean, // frame is muted audioPlaybackActive: boolean, // frame is playing audio + breakpoint: string, // breakpoint name for current tab size, specified in app/renderer/components/styles/tab.js computedThemeColor: string, // CSS computed theme color from the favicon frameKey: number, + hoverState: boolean, // wheter or not tab is being hovered icon: string, // favicon url isPrivate: boolean, // private browsing tab loading: boolean, diff --git a/docs/windowActions.md b/docs/windowActions.md index a189051549d..be1f887b98d 100644 --- a/docs/windowActions.md +++ b/docs/windowActions.md @@ -277,6 +277,30 @@ Dispatches a message to the store to set the tab page index. +### setTabBreakpoint(frameProps, breakpoint) + +Dispatches a message to the store to set the tab breakpoint. + +**Parameters** + +**frameProps**: `Object`, the frame properties for the webview in question. + +**breakpoint**: `string`, the tab breakpoint to change to + + + +### setTabHoverState(frameProps, hoverState) + +Dispatches a message to the store to set the current tab hover state. + +**Parameters** + +**frameProps**: `Object`, the frame properties for the webview in question. + +**hoverState**: `boolean`, whether or not mouse is over tab + + + ### setPreviewTabPageIndex(previewTabPageIndex) Dispatches a message to the store to set the tab page index being previewed. diff --git a/js/actions/windowActions.js b/js/actions/windowActions.js index 211c83bccb2..ed878a9d489 100644 --- a/js/actions/windowActions.js +++ b/js/actions/windowActions.js @@ -402,6 +402,34 @@ const windowActions = { }) }, + /** + * Dispatches a message to the store to set the tab breakpoint. + * + * @param {Object} frameProps - the frame properties for the webview in question. + * @param {string} breakpoint - the tab breakpoint to change to + */ + setTabBreakpoint: function (frameProps, breakpoint) { + dispatch({ + actionType: windowConstants.WINDOW_SET_TAB_BREAKPOINT, + frameProps, + breakpoint + }) + }, + + /** + * Dispatches a message to the store to set the current tab hover state. + * + * @param {Object} frameProps - the frame properties for the webview in question. + * @param {boolean} hoverState - whether or not mouse is over tab + */ + setTabHoverState: function (frameProps, hoverState) { + dispatch({ + actionType: windowConstants.WINDOW_SET_TAB_HOVER_STATE, + frameProps, + hoverState + }) + }, + /** * Dispatches a message to the store to set the tab page index being previewed. * diff --git a/js/components/tab.js b/js/components/tab.js index 240106976d0..52447e2a87c 100644 --- a/js/components/tab.js +++ b/js/components/tab.js @@ -5,6 +5,7 @@ const React = require('react') const ImmutableComponent = require('./immutableComponent') +const {StyleSheet, css} = require('aphrodite') const windowActions = require('../actions/windowActions') const locale = require('../l10n') @@ -18,14 +19,20 @@ const contextMenus = require('../contextMenus') const dnd = require('../dnd') const windowStore = require('../stores/windowStore') const ipc = require('electron').ipcRenderer +const throttle = require('../lib/throttle') -const {TabIcon, AudioTabIcon} = require('../../app/renderer/components/tabIcon') +const styles = require('../../app/renderer/components/styles/tab') +const {Favicon, AudioTabIcon, NewSessionIcon, + PrivateIcon, TabTitle, CloseTabIcon} = require('../../app/renderer/components/tabContent') +const {getTabBreakpoint, tabUpdateFrameRate} = require('../../app/renderer/lib/tabUtil') +const {isWindows} = require('../../app/common/lib/platformUtil') class Tab extends ImmutableComponent { constructor () { super() this.onMouseEnter = this.onMouseEnter.bind(this) this.onMouseLeave = this.onMouseLeave.bind(this) + this.onUpdateTabSize = this.onUpdateTabSize.bind(this) } get frame () { return windowStore.getFrame(this.props.tab.get('frameKey')) @@ -125,8 +132,11 @@ class Tab extends ImmutableComponent { } onMouseLeave () { - window.clearTimeout(this.hoverTimeout) - windowActions.setPreviewFrame(null) + if (this.props.previewTabs) { + window.clearTimeout(this.hoverTimeout) + windowActions.setPreviewFrame(null) + } + windowActions.setTabHoverState(this.frame, false) } onMouseEnter (e) { @@ -137,8 +147,11 @@ class Tab extends ImmutableComponent { // If user isn't in previewMode, we add a bit of delay to avoid tab from flashing out // as reported here: https://github.com/brave/browser-laptop/issues/1434 - this.hoverTimeout = - window.setTimeout(windowActions.setPreviewFrame.bind(null, this.frame), previewMode ? 0 : 200) + if (this.props.previewTabs) { + this.hoverTimeout = + window.setTimeout(windowActions.setPreviewFrame.bind(null, this.frame), previewMode ? 0 : 200) + } + windowActions.setTabHoverState(this.frame, true) } onClickTab (e) { @@ -150,48 +163,70 @@ class Tab extends ImmutableComponent { } } - render () { - // Style based on theme-color - const iconSize = 16 - let iconStyle = { - minWidth: iconSize, - width: iconSize - } - const activeTabStyle = {} - const backgroundColor = this.props.paintTabs && (this.props.tab.get('themeColor') || this.props.tab.get('computedThemeColor')) - if (this.props.isActive && backgroundColor) { - activeTabStyle.background = backgroundColor - const textColor = getTextColorForBackground(backgroundColor) - iconStyle.color = textColor - if (textColor) { - activeTabStyle.color = getTextColorForBackground(backgroundColor) - } - } + get themeColor () { + return this.props.paintTabs && + (this.props.tab.get('themeColor') || this.props.tab.get('computedThemeColor')) + } - const icon = this.props.tab.get('icon') - const defaultIcon = 'fa fa-file-o' + get tabSize () { + const tab = this.tabNode + // Avoid TypeError keeping it null until component is mounted + return tab && !this.isPinned ? tab.getBoundingClientRect().width : null + } - if (!this.loading && icon) { - iconStyle = Object.assign(iconStyle, { - backgroundImage: `url(${icon})`, - backgroundSize: iconSize, - height: iconSize - }) - } + get narrowView () { + const sizes = ['medium', 'mediumSmall', 'small', 'extraSmall', 'smallest'] + return sizes.includes(this.props.tab.get('breakpoint')) + } - let playIcon = false - let iconClass = null - if (this.props.tab.get('audioPlaybackActive') || this.props.tab.get('audioMuted')) { - if (this.props.tab.get('audioPlaybackActive') && !this.props.tab.get('audioMuted')) { - iconClass = 'fa fa-volume-up' - } else if (this.props.tab.get('audioPlaybackActive') && this.props.tab.get('audioMuted')) { - iconClass = 'fa fa-volume-off' - } - playIcon = true + get narrowestView () { + const sizes = ['extraSmall', 'smallest'] + return sizes.includes(this.props.tab.get('breakpoint')) + } + + get canPlayAudio () { + return this.props.tab.get('audioPlaybackActive') || this.props.tab.get('audioMuted') + } + + onUpdateTabSize () { + const currentSize = getTabBreakpoint(this.tabSize) + // Avoid changing state on unmounted component + // when user switch to a new tabSet + if (this.tabNode) { + windowActions.setTabBreakpoint(this.frame, currentSize) } + } + + componentWillMount () { + this.onUpdateTabSize() + } - const locationHasFavicon = this.props.tab.get('location') !== 'about:newtab' + componentDidMount () { + this.onUpdateTabSize() + window.addEventListener('resize', throttle(this.onUpdateTabSize, tabUpdateFrameRate)) + } + componentDidUpdate () { + this.tabSize + this.onUpdateTabSize() + } + + componentWillUnmount () { + this.onUpdateTabSize() + window.removeEventListener('resize', this.onUpdateTabSize) + } + + render () { + const perPageStyles = StyleSheet.create({ + themeColor: { + color: this.themeColor ? getTextColorForBackground(this.themeColor) : 'inherit', + background: this.themeColor ? this.themeColor : 'inherit', + ':hover': { + color: this.themeColor ? getTextColorForBackground(this.themeColor) : 'inherit', + background: this.themeColor ? this.themeColor : 'inherit' + } + } + }) return
-
+
{ this.tabNode = node }} draggable @@ -218,51 +265,30 @@ class Tab extends ImmutableComponent { onDragEnd={this.onDragEnd.bind(this)} onDragOver={this.onDragOver.bind(this)} onClick={this.onClickTab.bind(this)} - onContextMenu={contextMenus.onTabContextMenu.bind(this, this.frame)} - style={activeTabStyle}> - { - this.props.tab.get('isPrivate') - ? - : null - } - { - this.props.tab.get('partitionNumber') - ? - : null - } - { - locationHasFavicon - ?
- : null - } - { - playIcon - ? - : null - } - { - !this.isPinned - ?
- {this.displayValue} -
- : null - } - { - !this.isPinned - ? - : null - } + onContextMenu={contextMenus.onTabContextMenu.bind(this, this.frame)}> +
+ + + +
+ + +
} diff --git a/js/components/tabsToolbar.js b/js/components/tabsToolbar.js index d6723051625..429516aaf2e 100644 --- a/js/components/tabsToolbar.js +++ b/js/components/tabsToolbar.js @@ -65,6 +65,7 @@ class TabsToolbar extends ImmutableComponent { tabsPerTabPage={this.props.tabsPerTabPage} activeFrameKey={this.props.activeFrameKey} tabPageIndex={this.props.tabPageIndex} + tabBreakpoint={this.props.tabBreakpoint} currentTabs={currentTabs} previewTabPageIndex={this.props.previewTabPageIndex} startingFrameIndex={startingFrameIndex} diff --git a/js/constants/windowConstants.js b/js/constants/windowConstants.js index 5d0b1fb93df..fe3aa2b58f6 100644 --- a/js/constants/windowConstants.js +++ b/js/constants/windowConstants.js @@ -16,6 +16,8 @@ const windowConstants = { WINDOW_SET_PREVIEW_FRAME: _, WINDOW_SET_PREVIEW_TAB_PAGE_INDEX: _, WINDOW_SET_TAB_PAGE_INDEX: _, + WINDOW_SET_TAB_BREAKPOINT: _, + WINDOW_SET_TAB_HOVER_STATE: _, WINDOW_SET_IS_BEING_DRAGGED_OVER_DETAIL: _, WINDOW_TAB_MOVE: _, WINDOW_SET_THEME_COLOR: _, diff --git a/js/lib/throttle.js b/js/lib/throttle.js new file mode 100644 index 00000000000..d951c5cf00f --- /dev/null +++ b/js/lib/throttle.js @@ -0,0 +1,20 @@ +/* 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' + +function throttle (fn, limit) { + let waitingTime = false + return () => { + if (!waitingTime) { + fn.call() + waitingTime = true + setTimeout(() => { + waitingTime = false + }, limit) + } + } +} + +module.exports = throttle diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index 0a201b3620d..944ecfc1531 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -338,6 +338,7 @@ const doAction = (action) => { // Use the frameProps we passed in, or default to the active frame const frameProps = action.frameProps || frameStateUtil.getActiveFrame(windowState) const index = frameStateUtil.getFramePropsIndex(windowState.get('frames'), frameProps) + const hoverState = windowState.getIn(['frames', index, 'hoverState']) const activeFrameKey = frameStateUtil.getActiveFrame(windowState).get('key') windowState = windowState.merge(frameStateUtil.removeFrame( windowState.get('frames'), @@ -354,6 +355,18 @@ const doAction = (action) => { updateTabPageIndex(frameStateUtil.getActiveFrame(windowState)) windowState = windowState.deleteIn(['ui', 'tabs', 'fixTabWidth']) } + + const nextFrame = frameStateUtil.getFrameByIndex(windowState, index) + + // Copy the hover state if tab closed with mouse as long as we have a next frame + // This allow us to have closeTab button visible for sequential frames closing, until onMouseLeave event happens. + if (hoverState && nextFrame) { + doAction({ + actionType: windowConstants.WINDOW_SET_TAB_HOVER_STATE, + frameProps: nextFrame, + hoverState: hoverState + }) + } break case windowConstants.WINDOW_UNDO_CLOSED_FRAME: windowState = windowState.merge(frameStateUtil.undoCloseFrame(windowState, windowState.get('closedFrames'))) @@ -397,6 +410,14 @@ const doAction = (action) => { updateTabPageIndex(action.frameProps) } break + case windowConstants.WINDOW_SET_TAB_BREAKPOINT: + windowState = windowState.setIn(['frames', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'breakpoint'], action.breakpoint) + windowState = windowState.setIn(['tabs', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'breakpoint'], action.breakpoint) + break + case windowConstants.WINDOW_SET_TAB_HOVER_STATE: + windowState = windowState.setIn(['frames', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'hoverState'], action.hoverState) + windowState = windowState.setIn(['tabs', frameStateUtil.getFramePropsIndex(windowState.get('frames'), action.frameProps), 'hoverState'], action.hoverState) + break case windowConstants.WINDOW_SET_IS_BEING_DRAGGED_OVER_DETAIL: if (!action.dragOverKey) { windowState = windowState.deleteIn(['ui', 'dragging']) diff --git a/less/tabs.less b/less/tabs.less index bc403ff4f7c..4776c65af83 100644 --- a/less/tabs.less +++ b/less/tabs.less @@ -4,19 +4,6 @@ @import "variables.less"; -// Windows specific styles -.platform--win32 { - .tab { - color: #555; - - .tabTitle { - font-weight: 500; - font-size: 12px; - height: 18px; - } - } -} - .tabs { box-sizing: border-box; background: none; @@ -68,121 +55,6 @@ } } -.tab { - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.4), rgba(255, 255, 255, 0.1)); - border-radius: @borderRadiusTabs @borderRadiusTabs 0px 0px; - border-width: 1px 1px 0; - box-sizing: border-box; - color: #3B3B3B; - display: flex; - height: 23px; - margin-top: 2px; - transition: transform 200ms ease; - left: 0; - opacity: 1.0; - padding: 0; - width: 100%; - align-items: center; - justify-content: center; - border: 1px solid rgba(0, 0, 0, 0.0); - border-bottom: 1px; - - .tabTitle { - -webkit-user-select: none; - box-sizing: border-box; - display: inline-block; - font-size: 12px; - overflow: hidden; - text-overflow: ellipsis; - line-height: 16px; - white-space: nowrap; - vertical-align: middle; - width: calc(~'100% - 40px'); - height: 15px; - margin-left: 7px; - } - .tabIcon { - background-position: center; - background-repeat: no-repeat; - display: inline-block; - font-size: 12px; - text-align: center; - margin-left: 7px; - } - - .thumbnail { - display: none; - position: absolute; - top: 32px; - left: 0; - border: 1px solid #000; - padding: 10px; - background: #fff; - pointer-events: none; - z-index: @zindexTabsThumbnail; - } - - &.active { - background: linear-gradient(to bottom, white, @chromePrimary, ); - height: 25px; - margin-top: 1px; - box-shadow: inset 1px 1px 2px 0px white; - box-shadow: 0px -1px 4px 0px rgba(51, 51, 51, 0.12); - border: 1px solid #bbb; - border-bottom: 1px; - } - - &.private { - background: @privateTabBackground; - color: #fff; - &:not(.active) { - background: lighten(@privateTabBackground, 40%); - } - } - - &:hover { - .closeTab { - opacity: 0.5; - } - - .thumbnail { - display: block; - } - } - - &:not(.active):hover { - background: linear-gradient(to bottom, rgba(255, 255, 255, 0.8), rgba(250, 250, 250, 0.4)); - &.private { - background: lighten(@privateTabBackground, 20%); - } - } - - &.dragging { - &:hover { - .closeTab { - opacity: 0; - } - } - } - - .closeTab { - opacity: 0; - text-align: center; - width: 16px; - height: 16px; - margin-left: 4px; - margin-right: 4px; - - &:hover { - opacity: 1; - } - - border: 0px solid white; - border-radius: 50%; - z-index: @zindexTabs; - } -} - .tabArea { box-sizing: border-box; display: inline-block; diff --git a/test/about/adblockTest.js b/test/about/adblockTest.js index a6100bc1f25..ced8f50da2e 100644 --- a/test/about/adblockTest.js +++ b/test/about/adblockTest.js @@ -12,7 +12,7 @@ describe('about:adblock', function () { .waitForUrl(Brave.newTabUrl) .waitForBrowserWindow() .waitForVisible(urlInput) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .loadUrl(url) }) diff --git a/test/about/bookmarksManagerTest.js b/test/about/bookmarksManagerTest.js index c60e84124ef..fbf333dbe70 100644 --- a/test/about/bookmarksManagerTest.js +++ b/test/about/bookmarksManagerTest.js @@ -45,7 +45,7 @@ describe('about:bookmarks', function () { .addSite({ location: 'https://duckduckgo.com', title: 'duckduckgo', tags: bookmarkTag, parentFolderId: folderId, lastAccessedTime: lastVisit }, siteTags.BOOKMARK) .addSite({ location: 'https://google.com', title: 'Google', tags: bookmarkTag, parentFolderId: folderId, lastAccessedTime: lastVisit }, siteTags.BOOKMARK) .addSite({ location: 'https://bing.com', title: 'Bing', tags: bookmarkTag, parentFolderId: folderId, lastAccessedTime: lastVisit }, siteTags.BOOKMARK) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .url(aboutBookmarksUrl) } @@ -60,7 +60,7 @@ describe('about:bookmarks', function () { parentFolderId: 0, lastAccessedTime: lastVisit }, siteTags.BOOKMARK) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .url(aboutBookmarksUrl) } diff --git a/test/about/braveTest.js b/test/about/braveTest.js index 8746da0c2eb..b5e346ce18c 100644 --- a/test/about/braveTest.js +++ b/test/about/braveTest.js @@ -12,7 +12,7 @@ describe('about:brave tests', function () { .waitForUrl(Brave.newTabUrl) .waitForBrowserWindow() .waitForVisible(urlInput) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .loadUrl(url) }) diff --git a/test/about/componentUpdaterTest.js b/test/about/componentUpdaterTest.js index 6cb26da8755..0504856472b 100644 --- a/test/about/componentUpdaterTest.js +++ b/test/about/componentUpdaterTest.js @@ -11,7 +11,7 @@ describe('component updater', function () { .waitForBrowserWindow() .waitForVisible(urlInput) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') } describe('Google Widevine is disabled by default', function () { diff --git a/test/about/extensionsTest.js b/test/about/extensionsTest.js index d3e31743712..b71dd0f49c6 100644 --- a/test/about/extensionsTest.js +++ b/test/about/extensionsTest.js @@ -15,7 +15,7 @@ describe('about:extensions', function () { .waitForBrowserWindow() .waitForVisible(urlInput) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .url(aboutExtensionsUrl) } diff --git a/test/about/historyTest.js b/test/about/historyTest.js index d7cd4548ac3..6c434408d49 100644 --- a/test/about/historyTest.js +++ b/test/about/historyTest.js @@ -23,7 +23,7 @@ describe('about:history', function () { .addSite({ location: 'https://brave.com/test', customTitle: 'customTest' }) .addSite({ location: 'https://www.youtube.com' }) .addSite({ location: 'https://www.facebook.com' }) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .url(aboutHistoryUrl) } @@ -35,7 +35,7 @@ describe('about:history', function () { location: site, title: 'Page 1' }) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .url(aboutHistoryUrl) } diff --git a/test/about/newTabTest.js b/test/about/newTabTest.js index 7ba014813f5..ee259e252d2 100644 --- a/test/about/newTabTest.js +++ b/test/about/newTabTest.js @@ -60,7 +60,7 @@ describe('about:newtab tests', function () { .addSite({ location: 'about:preferences' }) .addSite({ location: 'about:safebrowsing' }) .addSite({ location: 'about:styles' }) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .url(aboutNewTabUrl) } @@ -68,7 +68,7 @@ describe('about:newtab tests', function () { function * waitForPageLoad (client) { yield client .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) } @@ -120,7 +120,7 @@ describe('about:newtab tests', function () { yield this.app.client .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .waitForVisible('.clock .time') .waitUntil(function () { diff --git a/test/about/stylesTest.js b/test/about/stylesTest.js index 2b72e2be20f..06f9864d661 100644 --- a/test/about/stylesTest.js +++ b/test/about/stylesTest.js @@ -12,7 +12,7 @@ describe('about:styles', function () { .waitForUrl(Brave.newTabUrl) .waitForBrowserWindow() .waitForVisible(urlInput) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .tabByIndex(0) .loadUrl(url) }) diff --git a/test/components/contentLoadingTest.js b/test/components/contentLoadingTest.js index 9f878bbcd08..4259ada4cfa 100644 --- a/test/components/contentLoadingTest.js +++ b/test/components/contentLoadingTest.js @@ -23,7 +23,7 @@ describe('content loading', function () { .url(page1) .windowByUrl(Brave.browserWindowUrl) .waitUntil(function () { - return this.getText('.tabTitle').then((title) => { + return this.getText('[data-test-id="tabTitle"]').then((title) => { return title === 'failed' }) }) @@ -36,7 +36,7 @@ describe('content loading', function () { .url(page1) .windowByUrl(Brave.browserWindowUrl) .waitUntil(function () { - return this.getText('.tabTitle').then((title) => { + return this.getText('[data-test-id="tabTitle"]').then((title) => { return title === 'fail' }) }) diff --git a/test/components/findbarTest.js b/test/components/findbarTest.js index dddf35ba088..4d123e509be 100644 --- a/test/components/findbarTest.js +++ b/test/components/findbarTest.js @@ -234,11 +234,12 @@ describe('findBar', function () { .showFindbar(true, 2) .waitForElementFocus(findBarInput) .setValue(findBarInput, 'abc') - .click('.tab') + .click('[data-test-id="tab"]') .waitUntil(function () { return this.getValue(findBarInput).then((val) => val === 'test') }) - .click('.closeTab') + .click('[data-test-id="tab"]') + .click('[data-test-id="closeTabIcon"]') .waitUntil(function () { return this.getValue(findBarInput).then((val) => val === 'abc') }) diff --git a/test/components/frameTest.js b/test/components/frameTest.js index 24d747fb0e3..2592d42fbce 100644 --- a/test/components/frameTest.js +++ b/test/components/frameTest.js @@ -55,7 +55,7 @@ describe('frame tests', function () { .click('#name') yield this.app.client .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') yield this.app.client .ipcSend('shortcut-set-active-frame-by-index', 0) .windowByUrl(Brave.browserWindowUrl) @@ -64,9 +64,9 @@ describe('frame tests', function () { }) it('inserts after the tab to clone', function * () { - this.tab1 = '.tabArea:nth-child(1) .tab[data-frame-key="1"]' - this.tab2 = '.tabArea:nth-child(2) .tab[data-frame-key="3"]' - this.tab3 = '.tabArea:nth-child(3) .tab[data-frame-key="2"]' + this.tab1 = '.tabArea:nth-child(1) [data-test-id="tab"][data-frame-key="1"]' + this.tab2 = '.tabArea:nth-child(2) [data-test-id="tab"][data-frame-key="3"]' + this.tab3 = '.tabArea:nth-child(3) [data-test-id="tab"][data-frame-key="2"]' yield this.app.client .windowByUrl(Brave.browserWindowUrl) .waitForExist(this.tab1) @@ -185,7 +185,7 @@ describe('frame tests', function () { .tabByIndex(0) .loadUrl(this.url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') .waitForExist(this.webview1) }) @@ -214,7 +214,7 @@ describe('frame tests', function () { yield setup(this.app.client) yield this.app.client .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="1"]') + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') }) it('loads an image', function * () { diff --git a/test/components/navigationBarTest.js b/test/components/navigationBarTest.js index fa7e3cf7009..b53e5e48d41 100644 --- a/test/components/navigationBarTest.js +++ b/test/components/navigationBarTest.js @@ -380,8 +380,8 @@ describe('navigationBar tests', function () { const pageWithNoFavicon = Brave.server.url('page_no_favicon.html') yield this.app.client.tabByUrl(Brave.newTabUrl).url(pageWithNoFavicon).waitForUrl(pageWithNoFavicon).windowParentByUrl(pageWithNoFavicon) yield this.app.client.waitUntil(function () { - return this.getAttribute(activeTabFavicon, 'class').then((className) => - className === 'tabIcon bookmarkFile fa fa-file-o') + return this.getAttribute('[data-test-id="defaultIcon"]', 'class').then((className) => + className === 'fa fa-file-o') }) }) }) @@ -759,8 +759,8 @@ describe('navigationBar tests', function () { it('Uses the default tab color when one is not specified', function * () { const page1Url = Brave.server.url('page1.html') yield this.app.client.tabByUrl(Brave.newTabUrl).url(page1Url).waitForUrl(page1Url).windowParentByUrl(page1Url) - let background = yield this.app.client.getCssProperty(activeTab, 'background') - assert.equal(background.value, 'rgba(0,0,0,0)linear-gradient(white,rgb(243,243,243))repeatscroll0%0%/autopadding-boxborder-box') + let background = yield this.app.client.getCssProperty('[data-test-active-tab]', 'background') + assert.equal(background.value, 'rgba(0,0,0,0)linear-gradient(rgb(255,255,255),rgb(243,243,243))repeatscroll0%0%/autopadding-boxborder-box') }) // We need a newer electron build first @@ -1089,7 +1089,7 @@ describe('navigationBar tests', function () { it('focuses on the urlbar', function * () { this.app.client - .waitForExist('.tab[data-frame-key="1"].active') + .waitForExist('[data-test-active-tab][data-frame-key="1"]') .waitForElementFocus(urlInput) }) }) diff --git a/test/components/pinnedTabTest.js b/test/components/pinnedTabTest.js index 377cfaaf7f2..8ff70eac46e 100644 --- a/test/components/pinnedTabTest.js +++ b/test/components/pinnedTabTest.js @@ -23,7 +23,7 @@ describe('pinnedTabs', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME, this.page1Url) .waitForUrl(this.page1Url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') .setPinned(this.page1Url, true) .waitForExist(pinnedTabsTabs) .waitForElementCount(pinnedTabsTabs, 1) @@ -31,12 +31,12 @@ describe('pinnedTabs', function () { }) it('creates when signaled', function * () { yield this.app.client - .waitForExist('.tab.isPinned[data-frame-key="2"]') + .waitForExist('[data-test-pinned-tab][data-frame-key="2"]') }) it('unpins and creates a non-pinned tab', function * () { yield this.app.client .setPinned(this.page1Url, false) - .waitForExist('.tab:not(.isPinned)[data-frame-key="2"]') + .waitForExist('[data-test-pinned-tab="false"][data-frame-key="2"]') .waitForElementCount(pinnedTabsTabs, 0) .waitForElementCount(tabsTabs, 2) }) @@ -45,7 +45,7 @@ describe('pinnedTabs', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME, this.page1Url) .waitForUrl(this.page1Url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="3"]') + .waitForExist('[data-test-id="tab"][data-frame-key="3"]') .setPinned(this.page1Url, true) .waitForElementCount(pinnedTabsTabs, 1) .waitForElementCount(tabsTabs, 2) @@ -61,13 +61,13 @@ describe('pinnedTabs', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME, this.page1Url) .waitForUrl(this.page1Url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') .setPinned(this.page1Url, true) .waitForExist(pinnedTabsTabs) .waitForElementCount(pinnedTabsTabs, 1) .waitForElementCount(tabsTabs, 1) .ipcSend(messages.SHORTCUT_NEW_FRAME, this.page1Url, {partitionNumber: 1}) - .waitForExist('.tab[data-frame-key="3"]') + .waitForExist('[data-test-id="tab"][data-frame-key="3"]') .setPinned(this.page1Url, true, {partitionNumber: 1}) .waitForElementCount(pinnedTabsTabs, 2) .waitForElementCount(tabsTabs, 1) @@ -84,11 +84,11 @@ describe('pinnedTabs', function () { .addSite({ location: page1Url }, siteTags.PINNED) .waitForUrl(page1Url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab.isPinned[data-frame-key="2"]') + .waitForExist('[data-test-pinned-tab][data-frame-key="2"]') .addSite({ location: page2Url }, siteTags.PINNED) .waitForUrl(page2Url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab.isPinned[data-frame-key="3"]') + .waitForExist('[data-test-pinned-tab][data-frame-key="3"]') }) it('creates when signaled', function * () { yield this.app.client.waitUntil(function () { @@ -132,7 +132,7 @@ describe('pinnedTabs', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME, this.page1Url) .waitForUrl(this.page1Url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') .setPinned(this.page1Url, true) .waitForExist(pinnedTabsTabs) }) @@ -171,13 +171,13 @@ describe('pinnedTabs', function () { .waitForElementCount(pinnedTabsTabs, 1) .waitForElementCount(tabsTabs, 1) .click(pinnedTabsTabs) - .waitForElementCount(pinnedTabsTabs + '.active', 1) + .waitForElementCount(pinnedTabsTabs + '[data-test-active-tab]', 1) }) it('close attempt retains pinned tab and selects next active frame', function * () { yield this.app.client - .waitForExist('.tab.active[data-frame-key="2"]') + .waitForExist('[data-test-active-tab][data-frame-key="2"]') .ipcSend(messages.SHORTCUT_CLOSE_FRAME) - .waitForExist('.tab.active[data-frame-key="1"]') + .waitForExist('[data-test-active-tab][data-frame-key="1"]') }) }) }) diff --git a/test/components/tabPagesTest.js b/test/components/tabPagesTest.js index ecd118ada50..4cafd7cce21 100644 --- a/test/components/tabPagesTest.js +++ b/test/components/tabPagesTest.js @@ -3,7 +3,7 @@ const Brave = require('../lib/brave') const appConfig = require('../../js/constants/appConfig') const settings = require('../../js/constants/settings') -const {urlInput, newFrameButton, tabsTabs, tabPage, tabPage1, tabPage2, closeTab, activeWebview} = require('../lib/selectors') +const {urlInput, newFrameButton, tabsTabs, tabPage, tabPage1, tabPage2, activeWebview} = require('../lib/selectors') describe('tab pages', function () { function * setup (client) { @@ -35,12 +35,15 @@ describe('tab pages', function () { }) it('shows no tab pages when you have only 1 page', function * () { - yield this.app.client.click(closeTab) + yield this.app.client + .waitForExist('[data-test-id="tab"][data-frame-key="1"]') + .click('[data-test-active-tab]') + .click('[data-test-id="closeTabIcon"]') .waitForElementCount(tabPage, 0) }) it('focuses active tab\'s page when closing last tab on page', function * () { - yield this.app.client.waitForVisible('.tab.active') + yield this.app.client.waitForVisible('[data-test-active-tab]') }) describe('allows changing to tab pages', function () { diff --git a/test/components/tabTest.js b/test/components/tabTest.js index 19f396db402..d92e18d566b 100644 --- a/test/components/tabTest.js +++ b/test/components/tabTest.js @@ -67,7 +67,7 @@ describe('tab tests', function () { it('creates a new tab when signaled', function * () { yield this.app.client .ipcSend(messages.SHORTCUT_NEW_FRAME) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') }) it('makes the non partitioned webview visible', function * () { @@ -85,7 +85,7 @@ describe('tab tests', function () { it('creates a new tab when clicked', function * () { yield this.app.client .click(newFrameButton) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') }) it('shows a context menu when long pressed (click and hold)', function * () { yield this.app.client @@ -111,9 +111,9 @@ describe('tab tests', function () { it('creates a new tab when signaled', function * () { yield this.app.client .ipcSend(messages.SHORTCUT_NEW_FRAME, 'about:blank', { openInForeground: false }) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') .ipcSend(messages.SHORTCUT_NEW_FRAME, 'about:blank') - .waitForExist('.tabArea + .tabArea + .tabArea .tab[data-frame-key="3"') + .waitForExist('.tabArea + .tabArea + .tabArea [data-test-id="tab"][data-frame-key="3"]') }) }) describe('respects parentFrameKey', function () { @@ -125,9 +125,9 @@ describe('tab tests', function () { it('creates a new tab when signaled', function * () { yield this.app.client .ipcSend(messages.SHORTCUT_NEW_FRAME, 'about:blank', { openInForeground: false }) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') .ipcSend(messages.SHORTCUT_NEW_FRAME, 'about:blank', { parentFrameKey: 1 }) - .waitForExist('.tabArea:nth-child(2) .tab[data-frame-key="3"]') + .waitForExist('.tabArea:nth-child(2) [data-test-id="tab"][data-frame-key="3"]') }) }) }) @@ -144,7 +144,7 @@ describe('tab tests', function () { }) it('creates a new private tab', function * () { yield this.app.client - .waitForExist('.tab.private[data-frame-key="2"]') + .waitForExist('[data-test-private-tab][data-frame-key="2"]') }) it('makes the private webview visible', function * () { yield this.app.client @@ -164,7 +164,7 @@ describe('tab tests', function () { }) it('creates a new session tab', function * () { yield this.app.client - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') }) it('makes the new session webview visible', function * () { yield this.app.client @@ -184,7 +184,7 @@ describe('tab tests', function () { }) it('creates a new session tab', function * () { yield this.app.client - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') }) it('makes the new session webview visible', function * () { yield this.app.client @@ -202,7 +202,7 @@ describe('tab tests', function () { it('can close a normal tab', function * () { yield this.app.client .waitForBrowserWindow() - .waitForExist('.tab.active[data-frame-key="1"]') + .waitForExist('[data-test-active-tab][data-frame-key="1"]') .ipcSend(messages.SHORTCUT_NEW_FRAME) .waitUntil(function () { return this.waitForUrl(Brave.newTabUrl) @@ -221,7 +221,7 @@ describe('tab tests', function () { .waitForBrowserWindow() .windowByUrl(Brave.browserWindowUrl) .ipcSend(messages.SHORTCUT_NEW_FRAME, Brave.server.url('page1.html'), {frameOpts: {unloaded: true, location: Brave.server.url('page1.html'), title: 'hi', tabId: null}, openInForeground: false}) - .waitForElementCount('.tab', 2) + .waitForElementCount('[data-test-id="tab"]', 2) // This ensures it's actually unloaded .waitForTabCount(1) .windowByUrl(Brave.browserWindowUrl) @@ -231,7 +231,7 @@ describe('tab tests', function () { it('should undo last closed tab', function * () { yield this.app.client .waitForBrowserWindow() - .waitForExist('.tab.active[data-frame-key="1"]') + .waitForExist('[data-test-active-tab][data-frame-key="1"]') .ipcSend(messages.SHORTCUT_NEW_FRAME, Brave.server.url('page1.html')) .waitUntil(function () { return this.waitForUrl(Brave.newTabUrl) @@ -265,24 +265,24 @@ describe('tab tests', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME, page1) .waitForUrl(page1) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') .ipcSend(messages.SHORTCUT_NEW_FRAME, page2) .waitForUrl(page2) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="3"]') + .waitForExist('[data-test-id="tab"][data-frame-key="3"]') }) it('shows a tab preview', function * () { yield this.app.client - .moveToObject('.tab[data-frame-key="2"]') - .moveToObject('.tab[data-frame-key="2"]', 3, 3) + .moveToObject('[data-test-id="tab"][data-frame-key="2"]') + .moveToObject('[data-test-id="tab"][data-frame-key="2"]', 3, 3) .waitForExist('.frameWrapper.isPreview webview[data-frame-key="2"]') .moveToObject(urlInput) }) it('does not show tab previews when setting is off', function * () { yield this.app.client.changeSetting(settings.SHOW_TAB_PREVIEWS, false) yield this.app.client - .moveToObject('.tab[data-frame-key="2"]') - .moveToObject('.tab[data-frame-key="2"]', 3, 3) + .moveToObject('[data-test-id="tab"][data-frame-key="2"]') + .moveToObject('[data-test-id="tab"][data-frame-key="2"]', 3, 3) try { yield this.app.client.waitForExist('.frameWrapper.isPreview webview[data-frame-key="2"]', 1000) } catch (e) { @@ -303,7 +303,7 @@ describe('tab tests', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME, url, {openInForeground: false}) .waitForUrl(url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="2"]') + .waitForExist('[data-test-id="tab"][data-frame-key="2"]') yield this.app.client.waitForExist('.frameWrapper:not(.isActive) webview[data-frame-key="2"]') }) it('changing new tab default makes new tabs open in background by default', function * () { @@ -313,7 +313,7 @@ describe('tab tests', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME, url, {openInForeground: false}) .waitForUrl(url) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="3"]') + .waitForExist('[data-test-id="tab"][data-frame-key="3"]') yield this.app.client.waitForExist('.frameWrapper.isActive webview[data-frame-key="3"]') }) }) @@ -325,7 +325,7 @@ describe('tab tests', function () { }) it('shows tab\'s icon when page is not about:blank or about:newtab ', function * () { - var url = Brave.server.url('page1.html') + var url = Brave.server.url('favicon.html') yield this.app.client .tabByIndex(0) .loadUrl(url) @@ -351,9 +351,9 @@ describe('tab tests', function () { it('has untitled text right away', function * () { yield this.app.client .ipcSend(messages.SHORTCUT_NEW_FRAME, 'about:blank', { openInForeground: false }) - .waitForVisible('.tab[data-frame-key="2"]') + .waitForVisible('[data-test-id="tab"][data-frame-key="2"]') // This should not be converted to a waitUntil - .getText('.tab[data-frame-key="2"]').then((val) => assert.equal(val, 'Untitled')) + .getText('[data-test-id="tab"][data-frame-key="2"]').then((val) => assert.equal(val, 'Untitled')) }) }) }) diff --git a/test/components/urlBarSuggestionsTest.js b/test/components/urlBarSuggestionsTest.js index cb7d0077f69..9c68d468d03 100644 --- a/test/components/urlBarSuggestionsTest.js +++ b/test/components/urlBarSuggestionsTest.js @@ -31,7 +31,7 @@ describe('urlBarSuggestions', function () { .ipcSend(messages.SHORTCUT_NEW_FRAME) .waitForUrl(Brave.newTabUrl) .windowByUrl(Brave.browserWindowUrl) - .waitForExist('.tab[data-frame-key="2"].active') + .waitForExist('[data-test-active-tab][data-frame-key="2"]') .waitForElementFocus(urlInput) }) diff --git a/test/components/urlBarTest.js b/test/components/urlBarTest.js index b4ff660761f..4456f20e538 100644 --- a/test/components/urlBarTest.js +++ b/test/components/urlBarTest.js @@ -413,11 +413,11 @@ describe('urlBar tests', function () { .waitUntil(function () { return this.getValue(urlInput).then((val) => val === coffee) }) - .click('.tab[data-frame-key="1"]') + .click('[data-test-id="tab"][data-frame-key="1"]') .waitUntil(function () { return this.getValue(urlInput).then((val) => val !== coffee) }) - .click('.tab[data-frame-key="2"]') + .click('[data-test-id="tab"][data-frame-key="2"]') .waitUntil(function () { return this.getValue(urlInput).then((val) => val === coffee) }) diff --git a/test/lib/selectors.js b/test/lib/selectors.js index c0494019f23..9ee20d4d685 100644 --- a/test/lib/selectors.js +++ b/test/lib/selectors.js @@ -4,19 +4,17 @@ module.exports = { closeButton: '.close-btn', urlInput: '#urlInput', activeWebview: '.frameWrapper.isActive webview', - activeTab: '.tab.active', - activeTabTitle: '.tab.active .tabTitle', - activeTabFavicon: '.tab.active .tabIcon', - pinnedTabs: '.pinnedTabs', - pinnedTabsTabs: '.pinnedTabs .tab', - tabsTabs: '.tabs .tab', + activeTabTitle: '[data-test-active-tab] [data-test-id="tabTitle"]', + activeTabFavicon: '[data-test-active-tab] [data-test-favicon]', + pinnedTabsTabs: '.pinnedTabs [data-test-id="tab"]', + tabsTabs: '.tabs [data-test-id="tab"]', navigator: '#navigator', navigatorLoadTime: '#navigator .loadTime', newFrameButton: '.tabs .newFrameButton', tabPage: '.tabPage', tabPage1: '[data-tab-page="0"]', tabPage2: '[data-tab-page="1"]', - closeTab: '.closeTab', + closeTab: '[data-test-id="closeTabIcon"]', urlbarIcon: '.urlbarIcon', urlBarSuggestions: '.urlBarSuggestions', titleBar: '#titleBar', diff --git a/test/unit/app/renderer/tabContentTest.js b/test/unit/app/renderer/tabContentTest.js new file mode 100644 index 00000000000..dc43e4f17d0 --- /dev/null +++ b/test/unit/app/renderer/tabContentTest.js @@ -0,0 +1,380 @@ +/* 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, after, it */ + +const mockery = require('mockery') +const {shallow} = require('enzyme') +const Immutable = require('immutable') +const assert = require('assert') +const fakeElectron = require('../../lib/fakeElectron') +const globalStyles = require('../../../../app/renderer/components/styles/global') +let Favicon, AudioTabIcon, PrivateIcon, NewSessionIcon, TabTitle, CloseTabIcon +require('../../braveUnit') + +describe('tabContent components', function () { + before(function () { + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + mockery.registerMock('electron', fakeElectron) + Favicon = require('../../../../app/renderer/components/tabContent').Favicon + AudioTabIcon = require('../../../../app/renderer/components/tabContent').AudioTabIcon + PrivateIcon = require('../../../../app/renderer/components/tabContent').PrivateIcon + NewSessionIcon = require('../../../../app/renderer/components/tabContent').NewSessionIcon + TabTitle = require('../../../../app/renderer/components/tabContent').TabTitle + CloseTabIcon = require('../../../../app/renderer/components/tabContent').CloseTabIcon + }) + after(function () { + mockery.disable() + }) + + const url1 = 'https://brave.com' + const favicon1 = 'https://brave.com/favicon.ico' + const pageTitle1 = 'Brave Software' + + describe('Favicon', function () { + it('should show favicon if page has one', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props()['data-test-favicon'], favicon1) + }) + it('should show a placeholder icon if page has no favicon', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.defaultIcon) + }) + it('should show a loading icon if page is still loading', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.loading) + }) + it('should not show favicon for new tab page', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().favicon, favicon1, 'does not show favicon') + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.loading, 'does not show loading icon') + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.defaultIcon, 'does not show default icon') + }) + }) + + describe('AudioTabIcon', function () { + it('should not show any audio icon if page has audio disabled', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOn) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOff) + }) + it('should show play icon if page has audio enabled', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.volumeOn) + }) + it('should not show play audio icon if tab size is too narrow', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOn) + }) + it('should show mute icon if page has audio muted', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.volumeOff) + }) + it('should not show mute icon if tab size is too narrow', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.volumeOff) + }) + }) + + describe('PrivateIcon', function () { + it('should show private icon if current tab is private', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.private) + }) + it('should not show private icon if current tab is not private', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.private) + }) + it('should not show private icon if mouse is over tab (avoid icon overflow)', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.private) + }) + it('should not show private icon if tab size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.private) + }) + }) + + describe('NewSessionIcon', function () { + it('should show new session icon if current tab is a new session tab', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + it('should not show new session icon if current tab is not private', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + it('should not show new session icon if mouse is over tab (avoid icon overflow)', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + it('should not show new session icon if tab size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.newSession) + }) + }) + + describe('Tab Title', function () { + it('should show text if page has a title', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.text(), pageTitle1) + }) + it('should not show text if tab is pinned', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + it('should not show text if size is largeMedium and location has audio and a secondary icon', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + it('should not show text if size is mediumSmall and location has a secondary icon', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + it('should not show text if size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.text(), pageTitle1) + }) + }) + + describe('CloseTabIcon', function () { + it('should show closeTab icon if mouse is over tab', function () { + const wrapper = shallow( + + ) + assert.equal(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + it('should not show closeTab icon if mouse is not over a tab', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + it('should not show closeTab icon if tab is pinned', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + it('should not show closeTab icon if tab size is too small', function () { + const wrapper = shallow( + + ) + assert.notEqual(wrapper.props().symbol, globalStyles.appIcons.closeTab) + }) + }) +}) diff --git a/test/unit/app/renderer/tabIconTest.js b/test/unit/app/renderer/tabIconTest.js deleted file mode 100644 index c0ea4596158..00000000000 --- a/test/unit/app/renderer/tabIconTest.js +++ /dev/null @@ -1,50 +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, before, after, it */ - -const mockery = require('mockery') -const {mount, shallow} = require('enzyme') -const assert = require('assert') -const sinon = require('sinon') -const fakeElectron = require('../../lib/fakeElectron') -let TabIcon, AudioTabIcon -require('../../braveUnit') - -describe('tabIcon component', function () { - before(function () { - mockery.enable({ - warnOnReplace: false, - warnOnUnregistered: false, - useCleanCache: true - }) - mockery.registerMock('electron', fakeElectron) - TabIcon = require('../../../../app/renderer/components/tabIcon').TabIcon - AudioTabIcon = require('../../../../app/renderer/components/tabIcon').AudioTabIcon - }) - after(function () { - mockery.disable() - }) - - describe('TabIcon', function () { - it('should call onClick callback', function () { - const onClick = sinon.spy() - const wrapper = shallow() - wrapper.find('div').simulate('click') - assert(onClick.calledOnce) - }) - }) - - describe('AudioTabIcon', function () { - it('should call onClick callback', function () { - const onClick = sinon.spy() - const wrapper = mount() - wrapper.find('div').simulate('click') - assert(onClick.calledOnce) - }) - it('should render a TabIcon with withBlueIcon prop', function () { - const wrapper = mount() - assert.ok(wrapper.find(TabIcon).props().withBlueIcon) - }) - }) -})