diff --git a/app/index.js b/app/index.js index c5c164fccb9..e82f8d73b0d 100644 --- a/app/index.js +++ b/app/index.js @@ -368,9 +368,8 @@ app.on('ready', () => { // reset the browser window. This will default to en-US if // not yet configured. locale.init(initialState.settings[settings.LANGUAGE], (strings) => { - Menu.init(AppStore.getState().get('settings'), {}) - // Initialize after localization strings async loaded + Menu.init(AppStore.getState().get('settings'), AppStore.getState().get('sites')) }) // Do this after loading the state @@ -443,7 +442,9 @@ app.on('ready', () => { }) ipcMain.on(messages.UPDATE_APP_MENU, (e, args) => { - Menu.init(AppStore.getState().get('settings'), args) + if (args && typeof args.bookmarked === 'boolean') { + Menu.updateBookmarkedStatus(args.bookmarked) + } }) ipcMain.on(messages.CHANGE_SETTING, (e, key, value) => { @@ -524,7 +525,7 @@ app.on('ready', () => { // save app state every 5 minutes regardless of update frequency setInterval(initiateSessionStateSave, 1000 * 60 * 5) AppStore.addChangeListener(() => { - Menu.init(AppStore.getState().get('settings')) + Menu.init(AppStore.getState().get('settings'), AppStore.getState().get('sites')) }) let masterKey diff --git a/app/menu.js b/app/menu.js index 5b695eac67a..a2c57888b09 100644 --- a/app/menu.js +++ b/app/menu.js @@ -11,66 +11,28 @@ const messages = require('../js/constants/messages') const settings = require('../js/constants/settings') const dialog = electron.dialog const appActions = require('../js/actions/appActions') +// const siteUtil = require('../js/state/siteUtil') const getSetting = require('../js/settings').getSetting const locale = require('./locale') - const isDarwin = process.platform === 'darwin' - const aboutUrl = 'https://brave.com/' -let menuArgs = {} -let lastSettingsState, lastArgs - +// Start off with an empty menu let appMenu = Menu.buildFromTemplate([]) Menu.setApplicationMenu(appMenu) -/** - * Sets up the menu. - * @param {Object} settingsState - Application settings state - * @param {Object} args - Arguments to initialize the menu with if any - * @param {boolean} state.bookmarked - Whether the current active page is - * bookmarked - */ -const init = (settingsState, args) => { - // The menu will always be called once localization is done - // so don't bother loading anything until it is done. - // Save out menuArgs in the meantime since they shuld persist across calls. - if (!locale.initialized) { - menuArgs = Object.assign(menuArgs, args || {}) - return - } +// Static menu definitions (initialized once in createMenu()) +let fileSubmenu, editSubmenu, viewSubmenu, braverySubmenu, windowSubmenu, helpSubmenu, debugSubmenu - // This needs to be within the init method to handle translations - const CommonMenu = require('../js/commonMenu') - - // Check for uneeded updates. - // Updating the menu when it is not needed causes the menu to close if expanded - // and also causes menu clicks to not work. So we don't want to update it a lot - // when app state changes, like when there are downloads. - // Note that settingsState is not used directly below, but getSetting uses it. - if (settingsState === lastSettingsState && args === lastArgs) { - return - } +// States which can trigger dynamic menus to change +let lastSettingsState, lastSites - lastSettingsState = settingsState - lastArgs = args - - menuArgs = Object.assign(menuArgs, args || {}) - // Create references to menu items that need to be updated dynamically - const bookmarkPageMenuItem = { - label: locale.translation('bookmarkPage'), - type: 'checkbox', - accelerator: 'CmdOrCtrl+D', - checked: menuArgs.bookmarked || false, - click: function (item, focusedWindow) { - var msg = bookmarkPageMenuItem.checked - ? messages.SHORTCUT_ACTIVE_FRAME_REMOVE_BOOKMARK - : messages.SHORTCUT_ACTIVE_FRAME_BOOKMARK - CommonMenu.sendToFocusedWindow(focusedWindow, [msg]) - } - } +// Used to hold the default value for "isBookmarked" (see createBookmarksSubmenu) +let initBookmarkChecked = false - const fileMenu = [ +// Submenu initialization +const createFileSubmenu = (CommonMenu) => { + const submenu = [ CommonMenu.newTabMenuItem(), CommonMenu.newPrivateTabMenuItem(), CommonMenu.newPartitionedTabMenuItem(), @@ -153,37 +115,25 @@ const init = (settingsState, args) => { CommonMenu.printMenuItem() ] - const helpMenu = [ - CommonMenu.reportAnIssueMenuItem(), - CommonMenu.separatorMenuItem, - CommonMenu.submitFeedbackMenuItem(), - { - label: locale.translation('spreadTheWord'), - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, - [messages.SHORTCUT_NEW_FRAME, aboutUrl]) - } - } - ] - if (!isDarwin) { - fileMenu.push(CommonMenu.separatorMenuItem) - fileMenu.push(CommonMenu.quitMenuItem()) - helpMenu.push(CommonMenu.separatorMenuItem) - helpMenu.push(CommonMenu.checkForUpdateMenuItem()) - helpMenu.push(CommonMenu.separatorMenuItem) - helpMenu.push(CommonMenu.aboutBraveMenuItem()) + submenu.push(CommonMenu.separatorMenuItem) + submenu.push(CommonMenu.quitMenuItem()) } - const editSubmenu = [{ - label: locale.translation('undo'), - accelerator: 'CmdOrCtrl+Z', - role: 'undo' - }, { - label: locale.translation('redo'), - accelerator: 'Shift+CmdOrCtrl+Z', - role: 'redo' - }, + return submenu +} + +const createEditSubmenu = (CommonMenu) => { + const submenu = [ + { + label: locale.translation('undo'), + accelerator: 'CmdOrCtrl+Z', + role: 'undo' + }, { + label: locale.translation('redo'), + accelerator: 'Shift+CmdOrCtrl+Z', + role: 'redo' + }, CommonMenu.separatorMenuItem, { label: locale.translation('cut'), @@ -228,299 +178,408 @@ const init = (settingsState, args) => { accelerator: 'Shift+CmdOrCtrl+G' }, CommonMenu.separatorMenuItem - // OSX inserts "start dictation" and "emoji and symbols" automatically + // NOTE: OSX inserts "start dictation" and "emoji and symbols" automatically ] if (!isDarwin) { - editSubmenu.push(CommonMenu.preferencesMenuItem()) + submenu.push(CommonMenu.preferencesMenuItem()) } - var template = [ + return submenu +} + +const createViewSubmenu = (CommonMenu) => { + return [ { - label: locale.translation('file'), - submenu: fileMenu + label: locale.translation('actualSize'), + accelerator: 'CmdOrCtrl+0', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_ZOOM_RESET]) + } }, { - label: locale.translation('edit'), - submenu: editSubmenu + label: locale.translation('zoomIn'), + accelerator: 'CmdOrCtrl+=', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_ZOOM_IN]) + } }, { - label: locale.translation('view'), + label: locale.translation('zoomOut'), + accelerator: 'CmdOrCtrl+-', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_ZOOM_OUT]) + } + }, + CommonMenu.separatorMenuItem, + /* + { + label: locale.translation('toolbars'), + visible: false submenu: [ - { - label: locale.translation('actualSize'), - accelerator: 'CmdOrCtrl+0', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_ZOOM_RESET]) - } - }, { - label: locale.translation('zoomIn'), - accelerator: 'CmdOrCtrl+=', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_ZOOM_IN]) - } - }, { - label: locale.translation('zoomOut'), - accelerator: 'CmdOrCtrl+-', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_ZOOM_OUT]) - } - }, - CommonMenu.separatorMenuItem, - /* - { - label: locale.translation('toolbars'), - visible: false - submenu: [ - {label: 'Favorites Bar', accelerator: 'Alt+CmdOrCtrl+B'}, - {label: 'Tab Bar'}, - {label: 'Address Bar', accelerator: 'Alt+CmdOrCtrl+A'}, - {label: 'Tab Previews', accelerator: 'Alt+CmdOrCtrl+P'} - ] - }, - CommonMenu.separatorMenuItem, - */ - { - label: locale.translation('stop'), - accelerator: isDarwin ? 'Cmd+.' : 'Esc', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_STOP]) - } - }, { - label: locale.translation('reloadPage'), - accelerator: 'CmdOrCtrl+R', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_RELOAD]) - } - }, { - label: locale.translation('cleanReload'), - accelerator: 'CmdOrCtrl+Shift+R', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_CLEAN_RELOAD]) - } - }, - CommonMenu.separatorMenuItem, - /* - { - label: locale.translation('readingView'), - visible: false, - accelerator: 'Alt+CmdOrCtrl+R' - }, { - label: locale.translation('tabManager'), - visible: false, - accelerator: 'Alt+CmdOrCtrl+M' - }, - CommonMenu.separatorMenuItem, - { - label: locale.translation('textEncoding'), - visible: false - submenu: [ - {label: 'Autodetect', submenu: []}, - CommonMenu.separatorMenuItem, - {label: 'Unicode'}, - {label: 'Western'}, - CommonMenu.separatorMenuItem, - {label: 'etc...'} - ] - }, - CommonMenu.separatorMenuItem, - */ - { - label: locale.translation('toggleDeveloperTools'), - accelerator: isDarwin ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_TOGGLE_DEV_TOOLS]) - } - }, - CommonMenu.separatorMenuItem, - { - label: locale.translation('toggleFullScreenView'), - accelerator: isDarwin ? 'Ctrl+Cmd+F' : 'F11', - click: function (item, focusedWindow) { - if (focusedWindow) { - // This doesn't seem to work but also doesn't throw errors... - focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) - } - } - } + {label: 'Favorites Bar', accelerator: 'Alt+CmdOrCtrl+B'}, + {label: 'Tab Bar'}, + {label: 'Address Bar', accelerator: 'Alt+CmdOrCtrl+A'}, + {label: 'Tab Previews', accelerator: 'Alt+CmdOrCtrl+P'} ] + }, + CommonMenu.separatorMenuItem, + */ + { + label: locale.translation('stop'), + accelerator: isDarwin ? 'Cmd+.' : 'Esc', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_STOP]) + } }, { - label: locale.translation('history'), - submenu: [ - { - label: locale.translation('home'), - accelerator: 'CmdOrCtrl+Shift+H', - click: function (item, focusedWindow) { - getSetting(settings.HOMEPAGE).split('|').forEach((homepage, i) => { - CommonMenu.sendToFocusedWindow(focusedWindow, - [i === 0 ? messages.SHORTCUT_ACTIVE_FRAME_LOAD_URL : messages.SHORTCUT_NEW_FRAME, homepage]) - }) - } - }, { - label: locale.translation('back'), - accelerator: 'CmdOrCtrl+[', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_BACK]) - } - }, { - label: locale.translation('forward'), - accelerator: 'CmdOrCtrl+]', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_FORWARD]) - } - }, - CommonMenu.separatorMenuItem, - CommonMenu.reopenLastClosedTabItem(), - { - label: locale.translation('reopenLastClosedWindow'), - accelerator: 'Alt+Shift+CmdOrCtrl+T', - click: function () { - process.emit(messages.UNDO_CLOSED_WINDOW) - } - }, - CommonMenu.separatorMenuItem, - /* - { - label: locale.translation('showAllHistory'), - accelerator: 'CmdOrCtrl+Y', - visible: false - }, - CommonMenu.separatorMenuItem, - */ - { - label: locale.translation('clearHistory'), - accelerator: 'Shift+CmdOrCtrl+Delete', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_OPEN_CLEAR_BROWSING_DATA_PANEL, {browserHistory: true}]) - } - }, { - label: locale.translation('clearCache'), - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_OPEN_CLEAR_BROWSING_DATA_PANEL, {cachedImagesAndFiles: true}]) - } - }, { - label: locale.translation('clearSiteData'), - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_OPEN_CLEAR_BROWSING_DATA_PANEL, {allSiteCookies: true, cachedImagesAndFiles: true}]) - } - } - ] + label: locale.translation('reloadPage'), + accelerator: 'CmdOrCtrl+R', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_RELOAD]) + } + }, { + label: locale.translation('cleanReload'), + accelerator: 'CmdOrCtrl+Shift+R', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_CLEAN_RELOAD]) + } + }, + CommonMenu.separatorMenuItem, + /* + { + label: locale.translation('readingView'), + visible: false, + accelerator: 'Alt+CmdOrCtrl+R' }, { - label: locale.translation('bookmarks'), + label: locale.translation('tabManager'), + visible: false, + accelerator: 'Alt+CmdOrCtrl+M' + }, + CommonMenu.separatorMenuItem, + { + label: locale.translation('textEncoding'), + visible: false submenu: [ - bookmarkPageMenuItem, - { - label: locale.translation('addToFavoritesBar'), - visible: false, - accelerator: 'Shift+CmdOrCtrl+D' - }, + {label: 'Autodetect', submenu: []}, CommonMenu.separatorMenuItem, - CommonMenu.bookmarksMenuItem(), - CommonMenu.bookmarksToolbarMenuItem(), + {label: 'Unicode'}, + {label: 'Western'}, CommonMenu.separatorMenuItem, - CommonMenu.importBookmarksMenuItem() + {label: 'etc...'} ] + }, + CommonMenu.separatorMenuItem, + */ + { + label: locale.translation('toggleDeveloperTools'), + accelerator: isDarwin ? 'Cmd+Alt+I' : 'Ctrl+Shift+I', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_TOGGLE_DEV_TOOLS]) + } + }, + CommonMenu.separatorMenuItem, + { + label: locale.translation('toggleFullScreenView'), + accelerator: isDarwin ? 'Ctrl+Cmd+F' : 'F11', + click: function (item, focusedWindow) { + if (focusedWindow) { + // This doesn't seem to work but also doesn't throw errors... + focusedWindow.setFullScreen(!focusedWindow.isFullScreen()) + } + } + } + ] +} + +const createHistorySubmenu = (CommonMenu) => { + return [ + { + label: locale.translation('home'), + accelerator: 'CmdOrCtrl+Shift+H', + click: function (item, focusedWindow) { + getSetting(settings.HOMEPAGE).split('|').forEach((homepage, i) => { + CommonMenu.sendToFocusedWindow(focusedWindow, + [i === 0 ? messages.SHORTCUT_ACTIVE_FRAME_LOAD_URL : messages.SHORTCUT_NEW_FRAME, homepage]) + }) + } }, { - label: locale.translation('bravery'), - submenu: [ - CommonMenu.braveryGlobalMenuItem(), - CommonMenu.braverySiteMenuItem() - ] + label: locale.translation('back'), + accelerator: 'CmdOrCtrl+[', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_BACK]) + } }, { - label: locale.translation('window'), - role: 'window', - submenu: [ - { - label: locale.translation('minimize'), - accelerator: 'CmdOrCtrl+M', - role: 'minimize' - // "Minimize all" added automatically - }, { - label: locale.translation('zoom'), - visible: false - }, - CommonMenu.separatorMenuItem, - { - label: locale.translation('selectNextTab'), - accelerator: 'Ctrl+Tab', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_NEXT_TAB]) - } - }, { - label: locale.translation('selectPreviousTab'), - accelerator: 'Ctrl+Shift+Tab', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_PREV_TAB]) - } - }, { - label: locale.translation('moveTabToNewWindow'), - visible: false - }, { - label: locale.translation('mergeAllWindows'), - visible: false - }, - CommonMenu.separatorMenuItem, - CommonMenu.bookmarksMenuItem(), - CommonMenu.downloadsMenuItem(), - CommonMenu.passwordsMenuItem(), - { - label: locale.translation('history'), - accelerator: 'CmdOrCtrl+Y', - visible: false - }, - CommonMenu.separatorMenuItem, - { - label: locale.translation('bringAllToFront'), - role: 'front' + label: locale.translation('forward'), + accelerator: 'CmdOrCtrl+]', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_FORWARD]) + } + }, + CommonMenu.separatorMenuItem, + CommonMenu.reopenLastClosedTabItem(), + { + label: locale.translation('reopenLastClosedWindow'), + accelerator: 'Alt+Shift+CmdOrCtrl+T', + click: function () { + process.emit(messages.UNDO_CLOSED_WINDOW) + } + }, + CommonMenu.separatorMenuItem, + /* + { + label: locale.translation('showAllHistory'), + accelerator: 'CmdOrCtrl+Y', + visible: false + }, + CommonMenu.separatorMenuItem, + */ + { + label: locale.translation('clearHistory'), + accelerator: 'Shift+CmdOrCtrl+Delete', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_OPEN_CLEAR_BROWSING_DATA_PANEL, {browserHistory: true}]) + } + }, { + label: locale.translation('clearCache'), + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_OPEN_CLEAR_BROWSING_DATA_PANEL, {cachedImagesAndFiles: true}]) + } + }, { + label: locale.translation('clearSiteData'), + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_OPEN_CLEAR_BROWSING_DATA_PANEL, {allSiteCookies: true, cachedImagesAndFiles: true}]) + } + } + ] +} + +const createBookmarksSubmenu = (CommonMenu) => { + return [ + { + label: locale.translation('bookmarkPage'), + type: 'checkbox', + accelerator: 'CmdOrCtrl+D', + checked: initBookmarkChecked, // NOTE: checked status is updated via updateBookmarkedStatus() + click: function (item, focusedWindow) { + var msg = item.checked + ? messages.SHORTCUT_ACTIVE_FRAME_REMOVE_BOOKMARK + : messages.SHORTCUT_ACTIVE_FRAME_BOOKMARK + CommonMenu.sendToFocusedWindow(focusedWindow, [msg]) + } + }, + { + label: locale.translation('addToFavoritesBar'), + visible: false, + accelerator: 'Shift+CmdOrCtrl+D' + }, + CommonMenu.separatorMenuItem, + CommonMenu.bookmarksManagerMenuItem(), + CommonMenu.bookmarksToolbarMenuItem(), + CommonMenu.separatorMenuItem, + CommonMenu.importBookmarksMenuItem() + ] + // TODO: commented out temporarily. + // Needs to be changed to update existing menu, not rebuild all menus (even if some are cached). + // + // ,CommonMenu.separatorMenuItem + // ].concat(CommonMenu.createBookmarkMenuItems(siteUtil.getBookmarks(lastSites))) +} + +const createWindowSubmenu = (CommonMenu) => { + return [ + { + label: locale.translation('minimize'), + accelerator: 'CmdOrCtrl+M', + role: 'minimize' + // "Minimize all" added automatically + }, { + label: locale.translation('zoom'), + visible: false + }, + CommonMenu.separatorMenuItem, + { + label: locale.translation('selectNextTab'), + accelerator: 'Ctrl+Tab', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_NEXT_TAB]) + } + }, { + label: locale.translation('selectPreviousTab'), + accelerator: 'Ctrl+Shift+Tab', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_PREV_TAB]) + } + }, { + label: locale.translation('moveTabToNewWindow'), + visible: false + }, { + label: locale.translation('mergeAllWindows'), + visible: false + }, + CommonMenu.separatorMenuItem, + CommonMenu.bookmarksManagerMenuItem(), + CommonMenu.downloadsMenuItem(), + CommonMenu.passwordsMenuItem(), + { + label: locale.translation('history'), + accelerator: 'CmdOrCtrl+Y', + visible: false + }, + CommonMenu.separatorMenuItem, + { + label: locale.translation('bringAllToFront'), + role: 'front' + } + ] +} + +const createHelpSubmenu = (CommonMenu) => { + const submenu = [ + CommonMenu.reportAnIssueMenuItem(), + CommonMenu.separatorMenuItem, + CommonMenu.submitFeedbackMenuItem(), + { + label: locale.translation('spreadTheWord'), + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, + [messages.SHORTCUT_NEW_FRAME, aboutUrl]) + } + } + ] + + if (!isDarwin) { + submenu.push(CommonMenu.separatorMenuItem) + submenu.push(CommonMenu.checkForUpdateMenuItem()) + submenu.push(CommonMenu.separatorMenuItem) + submenu.push(CommonMenu.aboutBraveMenuItem()) + } + + return submenu +} + +const createDebugSubmenu = (CommonMenu) => { + return [ + { + // Makes future renderer processes pause when they are created until a debugger appears. + // The console will print a message like: [84790:0710/201431:ERROR:child_process.cc(136)] Renderer (84790) paused waiting for debugger to attach. Send SIGUSR1 to unpause. + // And this means you should attach Xcode or whatever your debugger is to PID 84790 to unpause. + // To debug all renderer processes then add the appendSwitch call to app/index.js + label: 'append wait renderer switch', + click: function () { + electron.app.commandLine.appendSwitch('renderer-startup-dialog') + } + }, { + label: 'Crash main process', + click: function () { + process.crash() + } + }, { + label: 'Relaunch', + accelerator: 'Command+Alt+R', + click: function () { + electron.app.relaunch({args: process.argv.slice(1) + ['--relaunch']}) + electron.app.quit() + } + }, { + label: locale.translation('toggleBrowserConsole'), + accelerator: 'Shift+F8', + click: function (item, focusedWindow) { + if (focusedWindow) { + focusedWindow.toggleDevTools() } - ] + } }, { - label: locale.translation('help'), - role: 'help', - submenu: helpMenu + label: 'Toggle React Profiling', + accelerator: 'Alt+P', + click: function (item, focusedWindow) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.DEBUG_REACT_PROFILE]) + } } ] +} + +/** + * Get the electron MenuItem object based on its label + * @param {string} label - the text associated with the menu + * NOTE: label may be a localized string + */ +const getMenuItem = (label) => { + if (appMenu && appMenu.items && appMenu.items.length > 0) { + for (let i = 0; i < appMenu.items.length; i++) { + const menuItem = appMenu.items[i].submenu.items.find(function (item) { + return item.label === label + }) + if (menuItem) return menuItem + } + } + return null +} + +/** + * Called from navigationBar.js; used to update bookmarks menu status + * @param {boolean} isBookmarked - true if the currently viewed site is bookmarked + */ +const updateBookmarkedStatus = (isBookmarked) => { + const menuItem = getMenuItem(locale.translation('bookmarkPage')) + if (menuItem) { + menuItem.checked = isBookmarked + } + // menu may be rebuilt without the location changing + // this holds the last known status + initBookmarkChecked = isBookmarked +} + +/** + * Check for uneeded updates. + * Updating the menu when it is not needed causes the menu to close if expanded + * and also causes menu clicks to not work. So we don't want to update it a lot + * when app state changes, like when there are downloads. + * NOTE: settingsState is not used directly; it gets used indirectly via getSetting() + * @param {} + */ +const isUpdateNeeded = (settingsState, sites) => { + let stateChanged = false + if (settingsState !== lastSettingsState) { + lastSettingsState = settingsState + stateChanged = true + } + if (sites !== lastSites) { + lastSites = sites + stateChanged = true + } + return stateChanged +} + +/** + * Will only build the static items once + * Dynamic items (Bookmarks, History) are built each time + */ +const createMenu = (CommonMenu) => { + if (!fileSubmenu) { fileSubmenu = createFileSubmenu(CommonMenu) } + if (!editSubmenu) { editSubmenu = createEditSubmenu(CommonMenu) } + if (!viewSubmenu) { viewSubmenu = createViewSubmenu(CommonMenu) } + if (!braverySubmenu) { + braverySubmenu = [ + CommonMenu.braveryGlobalMenuItem(), + CommonMenu.braverySiteMenuItem() + ] + } + if (!windowSubmenu) { windowSubmenu = createWindowSubmenu(CommonMenu) } + if (!helpSubmenu) { helpSubmenu = createHelpSubmenu(CommonMenu) } + + // Creation of the menu. Notice Bookmarks and History are created each time. + const template = [ + { label: locale.translation('file'), submenu: fileSubmenu }, + { label: locale.translation('edit'), submenu: editSubmenu }, + { label: locale.translation('view'), submenu: viewSubmenu }, + { label: locale.translation('history'), submenu: createHistorySubmenu(CommonMenu) }, + { label: locale.translation('bookmarks'), submenu: createBookmarksSubmenu(CommonMenu) }, + { label: locale.translation('bravery'), submenu: braverySubmenu }, + { label: locale.translation('window'), submenu: windowSubmenu, role: 'window' }, + { label: locale.translation('help'), submenu: helpSubmenu, role: 'help' } + ] if (process.env.NODE_ENV === 'development') { - template.push({ - label: 'Debug', - submenu: [ - { - // Makes future renderer processes pause when they are created until a debugger appears. - // The console will print a message like: [84790:0710/201431:ERROR:child_process.cc(136)] Renderer (84790) paused waiting for debugger to attach. Send SIGUSR1 to unpause. - // And this means you should attach Xcode or whatever your debugger is to PID 84790 to unpause. - // To debug all renderer processes then add the appendSwitch call to app/index.js - label: 'append wait renderer switch', - click: function () { - electron.app.commandLine.appendSwitch('renderer-startup-dialog') - } - }, { - label: 'Crash main process', - click: function () { - process.crash() - } - }, { - label: 'Relaunch', - accelerator: 'Command+Alt+R', - click: function () { - electron.app.relaunch({args: process.argv.slice(1) + ['--relaunch']}) - electron.app.quit() - } - }, { - label: locale.translation('toggleBrowserConsole'), - accelerator: 'Shift+F8', - click: function (item, focusedWindow) { - if (focusedWindow) { - focusedWindow.toggleDevTools() - } - } - }, { - label: 'Toggle React Profiling', - accelerator: 'Alt+P', - click: function (item, focusedWindow) { - CommonMenu.sendToFocusedWindow(focusedWindow, [messages.DEBUG_REACT_PROFILE]) - } - } - ] - }) + if (!debugSubmenu) { debugSubmenu = createDebugSubmenu(CommonMenu) } + template.push({ label: 'Debug', submenu: debugSubmenu }) } if (isDarwin) { @@ -569,4 +628,32 @@ const init = (settingsState, args) => { oldMenu.destroy() } -module.exports.init = init +/** + * Sets up the menu. + * @param {Object} settingsState - Application settings state + * @param {List} - list of siteDetails + */ +const init = (settingsState, sites) => { + // The menu will always be called once localization is done + // so don't bother loading anything until it is done. + if (!locale.initialized) { + return + } + + if (!isUpdateNeeded(settingsState, sites)) { + return + } + + // This needs to be within the init method to handle translations + const CommonMenu = require('../js/commonMenu') + + // Only rebuild menu if it doesn't already exist (prevent leaking resources). + if (appMenu.items.length === 0) { + createMenu(CommonMenu) + } +} + +module.exports = { + init, + updateBookmarkedStatus +} diff --git a/docs/tests.md b/docs/tests.md index 16859578132..cfbb392fa90 100644 --- a/docs/tests.md +++ b/docs/tests.md @@ -15,13 +15,22 @@ To stay close to production, tests do not use the webpack dev server. To keep t npm run watch-test ## Running all tests +You can run ALL of the tests using: npm run test +If you'd prefer to only run the unit tests, you can do that using: + + npm run unittest + +If you only need to run the Spectron tests (used for UI testing), you can run: + + npm run uitest + ## Running a subset of tests You can run a subset of tests which match a `description` or `it` with: npm run test -- --grep="expression" -Where `expression` could be for example `^tabs` to match all tests which start with the word tabs. +Where `expression` could be for example `^tabs` to match all tests which start with the word tabs. This works for all testing modes (test, unittest, uitest). diff --git a/js/commonMenu.js b/js/commonMenu.js index 1cde0633d23..c137263fe58 100644 --- a/js/commonMenu.js +++ b/js/commonMenu.js @@ -13,6 +13,9 @@ const settings = require('./constants/settings') const getSetting = require('./settings').getSetting const issuesUrl = 'https://github.com/brave/browser-laptop/issues' const isDarwin = process.platform === 'darwin' +const siteTags = require('./constants/siteTags') +const siteUtil = require('./state/siteUtil') +const eventUtil = require('./lib/eventUtil') let electron try { @@ -175,7 +178,7 @@ module.exports.preferencesMenuItem = () => { } } -module.exports.bookmarksMenuItem = () => { +module.exports.bookmarksManagerMenuItem = () => { return { label: locale.translation('bookmarksManager'), accelerator: isDarwin ? 'CmdOrCtrl+Alt+B' : 'Ctrl+Shift+O', @@ -250,6 +253,46 @@ module.exports.importBookmarksMenuItem = () => { */ } +module.exports.createBookmarkMenuItems = (bookmarks, parentFolderId) => { + let filteredBookmarks + if (parentFolderId) { + filteredBookmarks = bookmarks.filter((bookmark) => bookmark.get('parentFolderId') === parentFolderId) + } else { + filteredBookmarks = bookmarks.filter((bookmark) => !bookmark.get('parentFolderId')) + } + + var payload = [] + filteredBookmarks.forEach((site) => { + if (site.get('tags').includes(siteTags.BOOKMARK) && site.get('location')) { + payload.push({ + // TODO include label made from favicon. It needs to be of type NativeImage + // which can be made using a Buffer / DataURL / local image + // the image will likely need to be included in the site data + // there was potentially concern about the size of the app state + // and as such there may need to be another mechanism or cache + // + // see: https://github.com/brave/browser-laptop/issues/3050 + label: site.get('customTitle') || site.get('title'), + click: (item, focusedWindow, e) => { + if (eventUtil.isForSecondaryAction(e)) { + module.exports.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_NEW_FRAME, site.get('location'), { openInForeground: !!e.shiftKey }]) + } else { + module.exports.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_LOAD_URL, site.get('location')]) + } + } + }) + } else if (siteUtil.isFolder(site)) { + const folderId = site.get('folderId') + const submenuItems = bookmarks.filter((bookmark) => bookmark.get('parentFolderId') === folderId) + payload.push({ + label: site.get('customTitle') || site.get('title'), + submenu: submenuItems.count() > 0 ? module.exports.createBookmarkMenuItems(bookmarks, folderId) : null + }) + } + }) + return payload +} + module.exports.reportAnIssueMenuItem = () => { return { label: locale.translation('reportAnIssue'), diff --git a/js/components/addEditBookmark.js b/js/components/addEditBookmark.js index 5d88aa8e672..9cc7a68e472 100644 --- a/js/components/addEditBookmark.js +++ b/js/components/addEditBookmark.js @@ -28,7 +28,7 @@ class AddEditBookmark extends ImmutableComponent { return ['about:blank', 'about:newtab'].includes(this.props.currentDetail.get('location')) } get isFolder () { - return this.props.currentDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER) + return siteUtil.isFolder(this.props.currentDetail) } updateFolders (props) { this.folders = siteUtil.getFolders(this.props.sites, props.currentDetail.get('folderId')) diff --git a/js/components/bookmarksToolbar.js b/js/components/bookmarksToolbar.js index 20d80163a05..9d0ad41fd4c 100644 --- a/js/components/bookmarksToolbar.js +++ b/js/components/bookmarksToolbar.js @@ -116,7 +116,7 @@ class BookmarkToolbarButton extends ImmutableComponent { } get isFolder () { - return this.props.bookmark.get('tags').includes(siteTags.BOOKMARK_FOLDER) + return siteUtil.isFolder(this.props.bookmark) } onContextMenu (e) { @@ -259,8 +259,8 @@ class BookmarksToolbar extends ImmutableComponent { contextMenus.onShowBookmarkFolderMenu(this.bookmarks, bookmark, this.activeFrame, e) } updateBookmarkData (props) { - this.bookmarks = props.sites - .filter((site) => site.get('tags').includes(siteTags.BOOKMARK) || site.get('tags').includes(siteTags.BOOKMARK_FOLDER)) + this.bookmarks = siteUtil.getBookmarks(props.sites) + const noParentItems = this.bookmarks .filter((bookmark) => !bookmark.get('parentFolderId')) let widthAccountedFor = 0 diff --git a/js/components/navigationBar.js b/js/components/navigationBar.js index 2bb6ad05afa..5c85e092732 100644 --- a/js/components/navigationBar.js +++ b/js/components/navigationBar.js @@ -11,7 +11,7 @@ const Button = require('./button') const UrlBar = require('./urlBar') const appActions = require('../actions/appActions') const windowActions = require('../actions/windowActions') -const {isSiteInList} = require('../state/siteUtil') +const {isSiteBookmarked} = require('../state/siteUtil') const siteTags = require('../constants/siteTags') const messages = require('../constants/messages') const settings = require('../constants/settings') @@ -76,11 +76,11 @@ class NavigationBar extends ImmutableComponent { get bookmarked () { return this.props.activeFrameKey !== undefined && - isSiteInList(this.props.sites, Immutable.fromJS({ + isSiteBookmarked(this.props.sites, Immutable.fromJS({ location: this.props.location, partitionNumber: this.props.partitionNumber, title: this.props.title - }), siteTags.BOOKMARK) + })) } get titleMode () { @@ -96,6 +96,8 @@ class NavigationBar extends ImmutableComponent { componentDidMount () { ipc.on(messages.SHORTCUT_ACTIVE_FRAME_BOOKMARK, () => this.onToggleBookmark(false)) ipc.on(messages.SHORTCUT_ACTIVE_FRAME_REMOVE_BOOKMARK, () => this.onToggleBookmark(true)) + // Set initial bookmark status in menu + ipc.send(messages.UPDATE_APP_MENU, {bookmarked: this.bookmarked}) } get showNoScriptInfo () { @@ -109,12 +111,14 @@ class NavigationBar extends ImmutableComponent { componentDidUpdate (prevProps) { // Update the app menu to reflect whether the current page is bookmarked const prevBookmarked = this.props.activeFrameKey !== undefined && - isSiteInList(prevProps.sites, Immutable.fromJS({ + isSiteBookmarked(prevProps.sites, Immutable.fromJS({ location: prevProps.location, partitionNumber: prevProps.partitionNumber, title: prevProps.title - }), siteTags.BOOKMARK) + })) + if (this.bookmarked !== prevBookmarked) { + // Used to update the Bookmarks menu (the checked status next to "Bookmark Page") ipc.send(messages.UPDATE_APP_MENU, {bookmarked: this.bookmarked}) } if (this.props.noScriptIsVisible && !this.showNoScriptInfo) { diff --git a/js/contextMenus.js b/js/contextMenus.js index 466d81b2ead..d58e3e8fced 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -110,7 +110,7 @@ function urlBarTemplateInit (searchDetail, activeFrame, e) { function tabsToolbarTemplateInit (activeFrame, closestDestinationDetail, isParent) { const menu = [ - CommonMenu.bookmarksMenuItem(), + CommonMenu.bookmarksManagerMenuItem(), CommonMenu.bookmarksToolbarMenuItem(), CommonMenu.separatorMenuItem ] @@ -213,7 +213,7 @@ function downloadsToolbarTemplateInit (downloadId, downloadItem) { function bookmarkTemplateInit (siteDetail, activeFrame) { const location = siteDetail.get('location') - const isFolder = siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER) + const isFolder = siteUtil.isFolder(siteDetail) const template = [] if (!isFolder) { @@ -573,7 +573,7 @@ function hamburgerTemplateInit (location, e) { { label: locale.translation('bookmarks'), submenu: [ - CommonMenu.bookmarksMenuItem(), + CommonMenu.bookmarksManagerMenuItem(), CommonMenu.bookmarksToolbarMenuItem(), CommonMenu.separatorMenuItem, CommonMenu.importBookmarksMenuItem() diff --git a/js/state/siteUtil.js b/js/state/siteUtil.js index cd0dc17cea5..0a05694ff10 100644 --- a/js/state/siteUtil.js +++ b/js/state/siteUtil.js @@ -8,6 +8,18 @@ const settings = require('../constants/settings') const getSetting = require('../settings').getSetting const urlParse = require('url').parse +const isBookmark = (tags) => { + return tags && tags.includes(siteTags.BOOKMARK) +} + +const isBookmarkFolder = (tags) => { + if (!tags) { + return false + } + return typeof tags === 'string' && tags === siteTags.BOOKMARK_FOLDER || + tags && typeof tags !== 'string' && tags.includes(siteTags.BOOKMARK_FOLDER) +} + /** * Obtains the index of the location in sites * @@ -16,10 +28,11 @@ const urlParse = require('url').parse * @return index of the site or -1 if not found. */ module.exports.getSiteIndex = function (sites, siteDetail, tags) { - let isBookmarkFolder = typeof tags === 'string' && tags === siteTags.BOOKMARK_FOLDER || - tags && typeof tags !== 'string' && tags.includes(siteTags.BOOKMARK_FOLDER) - if (isBookmarkFolder) { - return sites.findIndex((site) => site.get('folderId') === siteDetail.get('folderId') && site.get('tags').includes(siteTags.BOOKMARK_FOLDER)) + if (!sites || !siteDetail) { + return -1 + } + if (isBookmarkFolder(tags)) { + return sites.findIndex((site) => isBookmarkFolder(site.get('tags')) && site.get('folderId') === siteDetail.get('folderId')) } return sites.findIndex((site) => site.get('location') === siteDetail.get('location') && (site.get('partitionNumber') || 0) === (siteDetail.get('partitionNumber') || 0)) } @@ -31,12 +44,12 @@ module.exports.getSiteIndex = function (sites, siteDetail, tags) { * @param siteDetail The site to check if it's in the specified tag * @return true if the location is already bookmarked */ -module.exports.isSiteInList = function (sites, siteDetail, tag) { - const index = module.exports.getSiteIndex(sites, siteDetail, tag) +module.exports.isSiteBookmarked = function (sites, siteDetail) { + const index = module.exports.getSiteIndex(sites, siteDetail, siteTags.BOOKMARK) if (index === -1) { return false } - return sites.get(index).get('tags').includes(tag) + return isBookmark(sites.get(index).get('tags')) } const getNextFolderIdItem = (sites) => @@ -56,6 +69,10 @@ const getNextFolderIdItem = (sites) => }) module.exports.getNextFolderId = (sites) => { + const defaultFolderId = 0 + if (!sites) { + return defaultFolderId + } const maxIdItem = getNextFolderIdItem(sites) return (maxIdItem ? (maxIdItem.get('folderId') || 0) : 0) + 1 } @@ -97,7 +114,7 @@ module.exports.addSite = function (sites, siteDetail, tag, originalSiteDetail) { location: siteDetail.get('location'), // We don't want bookmarks and other site info being renamed on users if they already exist // The name should remain the same while it is bookmarked forever. - title: oldSite && tags.includes(siteTags.BOOKMARK) ? oldSite.get('title') : siteDetail.get('title') + title: oldSite && isBookmark(tags) ? oldSite.get('title') : siteDetail.get('title') }) if (folderId) { site = site.set('folderId', Number(folderId)) @@ -261,7 +278,10 @@ module.exports.isEquivalent = function (siteDetail1, siteDetail2) { * @return true if the site detail is a folder. */ module.exports.isFolder = function (siteDetail) { - return siteDetail.get('tags').includes(siteTags.BOOKMARK_FOLDER) + if (siteDetail) { + return isBookmarkFolder(siteDetail.get('tags')) + } + return false } /** @@ -330,6 +350,18 @@ module.exports.hasNoTagSites = function (sites) { return sites.findIndex((site) => !site.get('tags') || site.get('tags').size === 0) !== -1 } +/** + * Returns all sites that have a bookmark tag. + * @param sites The application state's Immutable sites list. + */ + +module.exports.getBookmarks = function (sites) { + if (sites) { + return sites.filter((site) => isBookmarkFolder(site.get('tags')) || isBookmark(site.get('tags'))) + } + return [] +} + /** * Gets a site origin (scheme + hostname + port) from a URL or null if not * available. diff --git a/package.json b/package.json index 98a73d25c94..38a286dad3c 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,8 @@ "start": "node ./tools/start.js --debug=5858 --enable-logging --v=0 --enable-extension-activity-logging --enable-sandbox-logging --enable-dcheck", "start-brk": "node ./tools/start.js --debug-brk=5858 -enable-logging --v=0 --enable-dcheck", "test": "NODE_ENV=test mocha --compilers js:babel-register --recursive $(find test -name '*Test.js')", + "unittest": "NODE_ENV=test mocha --compilers js:babel-register --recursive $(find ./test/unit -name '*Test.js')", + "uitest": "NODE_ENV=test mocha --compilers js:babel-register --recursive $(find test -name '*Test.js' -not -path 'test/unit/*')", "update-pdfjs": "rm -r app/extensions/pdfjs/; cp -r ../pdf.js/build/chromium/ app/extensions/pdfjs/", "vagrant-destroy-linux": "VAGRANT_CWD=./test/vms/vagrant/ubuntu-14.04 vagrant destroy", "vagrant-halt-linux": "VAGRANT_CWD=./test/vms/vagrant/ubuntu-14.04 vagrant halt", diff --git a/test/unit/braveUnit.js b/test/unit/braveUnit.js index bbd0e636af8..98a30f160d5 100644 --- a/test/unit/braveUnit.js +++ b/test/unit/braveUnit.js @@ -1 +1,2 @@ require('jsdom-global')() +require('babel-polyfill') diff --git a/test/unit/constants/settingsTest.js b/test/unit/constants/settingsTest.js index 0da033d326f..1d6f7d1a261 100644 --- a/test/unit/constants/settingsTest.js +++ b/test/unit/constants/settingsTest.js @@ -1,5 +1,6 @@ /* global describe, it */ +require('babel-polyfill') const settings = require('../../../js/constants/settings') const appConfig = require('../../../js/constants/appConfig') const assert = require('assert') diff --git a/test/unit/state/siteSettingsTest.js b/test/unit/state/siteSettingsTest.js index 03298863c08..cd6c7cd96f3 100644 --- a/test/unit/state/siteSettingsTest.js +++ b/test/unit/state/siteSettingsTest.js @@ -1,7 +1,6 @@ /* global describe, before, it */ const siteSettings = require('../../../js/state/siteSettings') -const siteUtil = require('../../../js/state/siteUtil') const assert = require('assert') const Immutable = require('immutable') let siteSettingsMap = new Immutable.Map() @@ -161,32 +160,3 @@ describe('siteSettings', function () { }) }) }) - -describe('siteUtil', function () { - describe('gets URL origin', function () { - it('gets URL origin for simple url', function () { - assert.strictEqual(siteUtil.getOrigin('https://abc.bing.com'), 'https://abc.bing.com') - }) - it('gets URL origin for url with port', function () { - assert.strictEqual(siteUtil.getOrigin('https://bing.com:443/?test=1#abc'), 'https://bing.com:443') - }) - it('gets URL origin for IP host', function () { - assert.strictEqual(siteUtil.getOrigin('http://127.0.0.1:443/?test=1#abc'), 'http://127.0.0.1:443') - }) - it('gets URL origin for slashless protocol URL', function () { - assert.strictEqual(siteUtil.getOrigin('about:test/foo'), 'about:test') - }) - it('returns null for invalid URL', function () { - assert.strictEqual(siteUtil.getOrigin('abc'), null) - }) - it('returns null for empty URL', function () { - assert.strictEqual(siteUtil.getOrigin(''), null) - }) - it('returns null for null URL', function () { - assert.strictEqual(siteUtil.getOrigin(null), null) - }) - it('returns correct result for URL with hostname that is a scheme', function () { - assert.strictEqual(siteUtil.getOrigin('http://http/test'), 'http://http') - }) - }) -}) diff --git a/test/unit/state/siteUtilTest.js b/test/unit/state/siteUtilTest.js new file mode 100644 index 00000000000..f191d332fed --- /dev/null +++ b/test/unit/state/siteUtilTest.js @@ -0,0 +1,421 @@ +/* global describe, it */ + +const siteTags = require('../../../js/constants/siteTags') +const siteUtil = require('../../../js/state/siteUtil') +const assert = require('assert') +const Immutable = require('immutable') + +const testUrl1 = 'https://brave.com/' +const testUrl2 = 'http://example.com/' + +describe('siteUtil', function () { + describe('getSiteIndex', function () { + it('returns -1 if sites is falsey', function () { + const siteDetail = Immutable.fromJS({ + folderId: 0 + }) + const index = siteUtil.getSiteIndex(null, siteDetail, siteTags.BOOKMARK_FOLDER) + assert.equal(index, -1) + }) + it('returns -1 if siteDetail is falsey', function () { + const sites = Immutable.fromJS([{ + folderId: 0, + tags: [siteTags.BOOKMARK_FOLDER] + }]) + const index = siteUtil.getSiteIndex(sites, null, siteTags.BOOKMARK_FOLDER) + assert.equal(index, -1) + }) + describe('matching `BOOKMARK_FOLDER`', function () { + it('returns index if folderId matches', function () { + const sites = Immutable.fromJS([{ + folderId: 0, + tags: [siteTags.BOOKMARK_FOLDER] + }]) + const siteDetail = Immutable.fromJS({ + folderId: 0 + }) + const index = siteUtil.getSiteIndex(sites, siteDetail, siteTags.BOOKMARK_FOLDER) + assert.equal(index, 0) + }) + it('returns -1 if folderId does not match', function () { + const sites = Immutable.fromJS([{ + folderId: 0, + tags: [siteTags.BOOKMARK_FOLDER] + }]) + const siteDetail = Immutable.fromJS({ + folderId: 1 + }) + const index = siteUtil.getSiteIndex(sites, siteDetail, siteTags.BOOKMARK_FOLDER) + assert.equal(index, -1) + }) + }) + describe('matching `BOOKMARK`', function () { + it('returns index if location and partitionNumber match', function () { + const sites = Immutable.fromJS([{ + location: testUrl1, + partitionNumber: 0, + tags: [siteTags.BOOKMARK] + }]) + const siteDetail = Immutable.fromJS({ + location: testUrl1, + partitionNumber: 0 + }) + const index = siteUtil.getSiteIndex(sites, siteDetail, siteTags.BOOKMARK) + assert.equal(index, 0) + }) + it('returns index if location matches and partitionNumber is NOT present', function () { + const sites = Immutable.fromJS([{ + location: testUrl1, + tags: [siteTags.BOOKMARK] + }]) + const siteDetail = Immutable.fromJS({ + location: testUrl1 + }) + const index = siteUtil.getSiteIndex(sites, siteDetail, siteTags.BOOKMARK) + assert.equal(index, 0) + }) + it('returns -1 if location does not match', function () { + const sites = Immutable.fromJS([{ + location: testUrl1, + tags: [siteTags.BOOKMARK] + }]) + const siteDetail = Immutable.fromJS({ + location: testUrl2 + }) + const index = siteUtil.getSiteIndex(sites, siteDetail, siteTags.BOOKMARK) + assert.equal(index, -1) + }) + it('returns -1 if partitionNumber does not match', function () { + const sites = Immutable.fromJS([{ + location: testUrl1, + partitionNumber: 0, + tags: [siteTags.BOOKMARK] + }]) + const siteDetail = Immutable.fromJS({ + location: testUrl1, + partitionNumber: 1 + }) + const index = siteUtil.getSiteIndex(sites, siteDetail, siteTags.BOOKMARK) + assert.equal(index, -1) + }) + }) + }) + + describe('isSiteBookmarked', function () { + it('returns true if site is bookmarked', function () { + const sites = Immutable.fromJS([{ + location: testUrl1, + tags: [siteTags.BOOKMARK] + }]) + const siteDetail = Immutable.fromJS({ + location: testUrl1 + }) + const result = siteUtil.isSiteBookmarked(sites, siteDetail) + assert.equal(result, true) + }) + it('returns false if site is not bookmarked', function () { + const sites = Immutable.fromJS([{ + location: testUrl1, + tags: [siteTags.BOOKMARK] + }]) + const siteDetail = Immutable.fromJS({ + location: testUrl2 + }) + const result = siteUtil.isSiteBookmarked(sites, siteDetail) + assert.equal(result, false) + }) + it('returns false if site is a bookmark folder', function () { + const sites = Immutable.fromJS([{ + folderId: 0, + tags: [siteTags.BOOKMARK_FOLDER] + }]) + const siteDetail = Immutable.fromJS({ + folderId: 0 + }) + const result = siteUtil.isSiteBookmarked(sites, siteDetail) + assert.equal(result, false) + }) + }) + + describe('getNextFolderId', function () { + it('returns the next possible folderId', function () { + const sites = Immutable.fromJS([{ + folderId: 0, + tags: [siteTags.BOOKMARK_FOLDER] + }, { + folderId: 1, + tags: [siteTags.BOOKMARK_FOLDER] + }]) + assert.equal(siteUtil.getNextFolderId(sites), 2) + }) + it('returns default (0) if sites is falsey', function () { + assert.equal(siteUtil.getNextFolderId(null), 0) + }) + }) + + describe('addSite', function () { + describe('when adding a new siteDetail', function () { + it('returns the updated site list which includes the new site', function () { + const sites = Immutable.fromJS([]) + const siteDetail = Immutable.fromJS({ + lastAccessedTime: 123, + tags: [siteTags.BOOKMARK], + location: testUrl1, + title: 'sample' + }) + const processedSites = siteUtil.addSite(sites, siteDetail, siteTags.BOOKMARK) + const expectedSites = sites.push(siteDetail) + assert.deepEqual(processedSites, expectedSites) + }) + }) + }) + + describe('removeSite', function () { + it('removes the siteDetail from the site list (by removing the tag)', function () { + const siteDetail = { + tags: [siteTags.BOOKMARK], + location: testUrl1 + } + const sites = Immutable.fromJS([siteDetail]) + const processedSites = siteUtil.removeSite(sites, Immutable.fromJS(siteDetail), siteTags.BOOKMARK) + const expectedSites = sites.setIn([0, 'parentFolderId'], 0).setIn([0, 'tags'], Immutable.List([])) + assert.deepEqual(processedSites, expectedSites) + }) + }) + + describe('moveSite', function () { + }) + + describe('getDetailFromFrame', function () { + it('returns an Immutable object with all expected properties', function () { + const frame = Immutable.fromJS({ + location: testUrl1, + title: 'test123', + partitionNumber: 8, + tag: siteTags.BOOKMARK, + favicon: testUrl1 + 'favicon.ico' + }) + const siteDetail = siteUtil.getDetailFromFrame(frame, siteTags.BOOKMARK) + assert.equal(siteDetail.get('location'), frame.get('location')) + assert.equal(siteDetail.get('title'), frame.get('title')) + assert.equal(siteDetail.get('partitionNumber'), frame.get('partitionNumber')) + assert.deepEqual(siteDetail.get('tags'), Immutable.fromJS([frame.get('tag')])) + assert.equal(siteDetail.get('icon'), frame.get('icon')) + }) + it('properly sets location for pinned sites', function () { + const frame = Immutable.fromJS({ + pinnedLocation: testUrl1, + tag: siteTags.PINNED + }) + const siteDetail = siteUtil.getDetailFromFrame(frame, siteTags.PINNED) + assert.equal(siteDetail.get('location'), frame.get('pinnedLocation')) + }) + }) + + describe('toFrameOpts', function () { + it('returns a plain javascript object with location and partitionNumber', function () { + const siteDetail = Immutable.fromJS({ + location: testUrl1, + partitionNumber: 5 + }) + const result = siteUtil.toFrameOpts(siteDetail) + assert.equal(result.location, siteDetail.get('location')) + assert.equal(result.partitionNumber, siteDetail.get('partitionNumber')) + }) + }) + + describe('isEquivalent', function () { + it('returns true if both siteDetail objects are identical', function () { + const siteDetail1 = Immutable.fromJS({ + location: testUrl1, + partitionNumber: 0, + tags: [siteTags.BOOKMARK] + }) + const siteDetail2 = Immutable.fromJS({ + location: testUrl1, + partitionNumber: 0, + tags: [siteTags.BOOKMARK] + }) + assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), true) + }) + it('returns false if one object is a folder and the other is not', function () { + const siteDetail1 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK] + }) + const siteDetail2 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK_FOLDER] + }) + assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) + }) + it('returns false if both are folders and have a different folderId', function () { + const siteDetail1 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK_FOLDER], + folderId: 0 + }) + const siteDetail2 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK_FOLDER], + folderId: 1 + }) + assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) + }) + it('returns false if both are bookmarks and have a different location', function () { + const siteDetail1 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK], + location: testUrl1 + }) + const siteDetail2 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK], + location: 'http://example.com/' + }) + assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) + }) + it('returns false if both are bookmarks and have a different partitionNumber', function () { + const siteDetail1 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK], + location: testUrl1, + partitionNumber: 0 + }) + const siteDetail2 = Immutable.fromJS({ + tags: [siteTags.BOOKMARK], + location: testUrl2, + partitionNumber: 1 + }) + assert.equal(siteUtil.isEquivalent(siteDetail1, siteDetail2), false) + }) + }) + + describe('isFolder', function () { + it('returns true if the input is a siteDetail and has a BOOKMARK_FOLDER tag', function () { + const siteDetail = Immutable.fromJS({ + tags: [siteTags.BOOKMARK_FOLDER] + }) + assert.equal(siteUtil.isFolder(siteDetail), true) + }) + it('returns false if the input does not have a BOOKMARK_FOLDER tag', function () { + const siteDetail = Immutable.fromJS({ + tags: [siteTags.BOOKMARK] + }) + assert.equal(siteUtil.isFolder(siteDetail), false) + }) + it('returns false if there is no `tags` property', function () { + const siteDetail = Immutable.fromJS({ + notTags: null + }) + assert.equal(siteUtil.isFolder(siteDetail), false) + }) + it('returns false if the input is null', function () { + assert.equal(siteUtil.isFolder(null), false) + }) + it('returns false if the input is undefined', function () { + assert.equal(siteUtil.isFolder(), false) + }) + }) + + describe('getFolders', function () { + }) + + describe('filterOutNonRecents', function () { + }) + + describe('filterSitesRelativeTo', function () { + }) + + describe('clearSitesWithoutTags', function () { + it('does not remove sites which have a valid `tags` property', function () { + const sites = [ + Immutable.fromJS({ + tags: [siteTags.BOOKMARK_FOLDER] + }), + Immutable.fromJS({ + tags: [siteTags.BOOKMARK] + })] + const processedSites = siteUtil.clearSitesWithoutTags(sites) + assert.deepEqual(sites, processedSites) + }) + }) + + describe('hasNoTagSites', function () { + it('returns true if the ANY sites in the provided list are missing a `tags` property', function () { + const sites = [ + Immutable.fromJS({ + location: 'https://brave.com' + })] + assert.equal(siteUtil.hasNoTagSites(sites), true) + }) + it('returns true if the ANY sites in the provided list have an empty `tags` property', function () { + const sites = [ + Immutable.fromJS({ + tags: [] + })] + assert.equal(siteUtil.hasNoTagSites(sites), true) + }) + it('returns false if all sites have a valid `tags` property', function () { + const sites = [ + Immutable.fromJS({ + tags: [siteTags.BOOKMARK_FOLDER] + }), + Immutable.fromJS({ + tags: [siteTags.BOOKMARK] + })] + assert.equal(siteUtil.hasNoTagSites(sites), false) + }) + }) + + describe('getBookmarks', function () { + it('returns items which are tagged either `BOOKMARK_FOLDER` or `BOOKMARK`', function () { + const sites = [ + Immutable.fromJS({ + tags: [siteTags.BOOKMARK_FOLDER] + }), + Immutable.fromJS({ + tags: [siteTags.BOOKMARK] + })] + const processedSites = siteUtil.getBookmarks(sites) + assert.deepEqual(sites, processedSites) + }) + it('excludes items which are NOT tagged `BOOKMARK_FOLDER` or `BOOKMARK`', function () { + const sites = [ + Immutable.fromJS({ + tags: ['unknown1'] + }), + Immutable.fromJS({ + tags: ['unknown2'] + })] + const expectedSites = [] + const processedSites = siteUtil.getBookmarks(sites) + assert.deepEqual(expectedSites, processedSites) + }) + it('returns empty list if input was falsey', function () { + const processedSites = siteUtil.getBookmarks(null) + const expectedSites = [] + assert.deepEqual(processedSites, expectedSites) + }) + }) + + describe('getOrigin', function () { + it('gets URL origin for simple url', function () { + assert.strictEqual(siteUtil.getOrigin('https://abc.bing.com'), 'https://abc.bing.com') + }) + it('gets URL origin for url with port', function () { + assert.strictEqual(siteUtil.getOrigin('https://bing.com:443/?test=1#abc'), 'https://bing.com:443') + }) + it('gets URL origin for IP host', function () { + assert.strictEqual(siteUtil.getOrigin('http://127.0.0.1:443/?test=1#abc'), 'http://127.0.0.1:443') + }) + it('gets URL origin for slashless protocol URL', function () { + assert.strictEqual(siteUtil.getOrigin('about:test/foo'), 'about:test') + }) + it('returns null for invalid URL', function () { + assert.strictEqual(siteUtil.getOrigin('abc'), null) + }) + it('returns null for empty URL', function () { + assert.strictEqual(siteUtil.getOrigin(''), null) + }) + it('returns null for null URL', function () { + assert.strictEqual(siteUtil.getOrigin(null), null) + }) + it('returns correct result for URL with hostname that is a scheme', function () { + assert.strictEqual(siteUtil.getOrigin('http://http/test'), 'http://http') + }) + }) +})