diff --git a/app/extensions/brave/about-history.html b/app/extensions/brave/about-history.html new file mode 100644 index 00000000000..b8ea8b77eef --- /dev/null +++ b/app/extensions/brave/about-history.html @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + +
+ + diff --git a/app/extensions/brave/locales/en-US/history.properties b/app/extensions/brave/locales/en-US/history.properties new file mode 100644 index 00000000000..e89afaabd05 --- /dev/null +++ b/app/extensions/brave/locales/en-US/history.properties @@ -0,0 +1,2 @@ +historyTitle=History +history=History diff --git a/app/extensions/brave/locales/en-US/menu.properties b/app/extensions/brave/locales/en-US/menu.properties index 2137b03fe07..e3db72f153f 100644 --- a/app/extensions/brave/locales/en-US/menu.properties +++ b/app/extensions/brave/locales/en-US/menu.properties @@ -43,10 +43,12 @@ home=Home back=Back forward=Forward reopenLastClosedWindow=Reopen Last Closed Window -showAllHistory=Show All History +showAllHistory=Show History clearHistory=Clear History... clearCache=Clear Cache... clearSiteData=Clear All Cookies and Site Data... +recentlyClosed=Recently Closed +recentlyVisited=Recently Visited bookmarks=Bookmarks addToFavoritesBar=Add to Favorites Bar window=Window diff --git a/app/index.js b/app/index.js index 9bb3b764fa2..e7ee7aef3a7 100644 --- a/app/index.js +++ b/app/index.js @@ -59,6 +59,7 @@ const siteSettings = require('../js/state/siteSettings') const spellCheck = require('./spellCheck') const flash = require('../js/flash') const contentSettings = require('../js/state/contentSettings') +const FrameStateUtil = require('../js/state/frameStateUtil') // Used to collect the per window state when shutting down the application let perWindowState = [] @@ -294,6 +295,19 @@ app.on('ready', () => { saveIfAllCollected() }) + // Window state must be fetched from main process; this is fired once it's retrieved + ipcMain.on(messages.RESPONSE_MENU_DATA_FOR_WINDOW, (wnd, windowState) => { + if (windowState) { + const activeFrame = FrameStateUtil.getActiveFrame(Immutable.fromJS(windowState)) + const windowData = Immutable.fromJS({ + location: activeFrame.get('location'), + closedFrames: windowState.closedFrames + }) + + Menu.init(AppStore.getState(), windowData) + } + }) + ipcMain.on(messages.LAST_WINDOW_STATE, (wnd, data) => { if (data) { lastWindowState = data @@ -367,8 +381,7 @@ app.on('ready', () => { // reset the browser window. This will default to en-US if // not yet configured. locale.init(initialState.settings[settings.LANGUAGE], (strings) => { - // Initialize after localization strings async loaded - Menu.init(AppStore.getState().get('settings'), AppStore.getState().get('sites')) + Menu.init(AppStore.getState(), null) }) // Do this after loading the state @@ -439,9 +452,9 @@ app.on('ready', () => { } }) - ipcMain.on(messages.UPDATE_APP_MENU, (e, args) => { - if (args && typeof args.bookmarked === 'boolean') { - Menu.updateBookmarkedStatus(args.bookmarked) + ipcMain.on(messages.UPDATE_MENU_BOOKMARKED_STATUS, (e, isBookmarked) => { + if (typeof isBookmarked === 'boolean') { + Menu.updateBookmarkedStatus(isBookmarked) } }) @@ -522,8 +535,13 @@ 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'), AppStore.getState().get('sites')) + if (BrowserWindow.getFocusedWindow()) { + BrowserWindow.getFocusedWindow().webContents.send(messages.REQUEST_MENU_DATA_FOR_WINDOW) + } else { + Menu.init(AppStore.getState(), null) + } }) let masterKey diff --git a/app/locale.js b/app/locale.js index 1569f2dae8f..c5bac0f84a9 100644 --- a/app/locale.js +++ b/app/locale.js @@ -110,6 +110,8 @@ var rendererIdentifiers = function () { 'clearCache', 'clearHistory', 'clearSiteData', + 'recentlyClosed', + 'recentlyVisited', 'bookmarks', 'addToFavoritesBar', 'window', diff --git a/app/menu.js b/app/menu.js index a2c57888b09..4226ae49520 100644 --- a/app/menu.js +++ b/app/menu.js @@ -4,16 +4,19 @@ 'use strict' +const Immutable = require('immutable') const electron = require('electron') const appConfig = require('../js/constants/appConfig') const Menu = electron.Menu +const MenuItem = electron.MenuItem 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 menuUtil = require('../js/lib/menuUtil') const getSetting = require('../js/settings').getSetting const locale = require('./locale') +const {isSiteBookmarked} = require('../js/state/siteUtil') const isDarwin = process.platform === 'darwin' const aboutUrl = 'https://brave.com/' @@ -21,14 +24,8 @@ const aboutUrl = 'https://brave.com/' let appMenu = Menu.buildFromTemplate([]) Menu.setApplicationMenu(appMenu) -// Static menu definitions (initialized once in createMenu()) -let fileSubmenu, editSubmenu, viewSubmenu, braverySubmenu, windowSubmenu, helpSubmenu, debugSubmenu - -// States which can trigger dynamic menus to change -let lastSettingsState, lastSites - -// Used to hold the default value for "isBookmarked" (see createBookmarksSubmenu) -let initBookmarkChecked = false +// Value for history menu's "Bookmark Page" menu item; see createBookmarksSubmenu() +let isBookmarkChecked = false // Submenu initialization const createFileSubmenu = (CommonMenu) => { @@ -290,7 +287,7 @@ const createViewSubmenu = (CommonMenu) => { } const createHistorySubmenu = (CommonMenu) => { - return [ + let submenu = [ { label: locale.translation('home'), accelerator: 'CmdOrCtrl+Shift+H', @@ -349,6 +346,20 @@ const createHistorySubmenu = (CommonMenu) => { } } ] + + submenu = submenu.concat(menuUtil.createRecentlyClosedMenuItems()) + + submenu.push( + // TODO: recently visited + // CommonMenu.separatorMenuItem, + // { + // label: locale.translation('recentlyVisited'), + // enabled: false + // }, + CommonMenu.separatorMenuItem, + CommonMenu.historyMenuItem()) + + return submenu } const createBookmarksSubmenu = (CommonMenu) => { @@ -357,7 +368,7 @@ const createBookmarksSubmenu = (CommonMenu) => { label: locale.translation('bookmarkPage'), type: 'checkbox', accelerator: 'CmdOrCtrl+D', - checked: initBookmarkChecked, // NOTE: checked status is updated via updateBookmarkedStatus() + checked: isBookmarkChecked, // NOTE: checked status is updated via updateBookmarkedStatus() click: function (item, focusedWindow) { var msg = item.checked ? messages.SHORTCUT_ACTIVE_FRAME_REMOVE_BOOKMARK @@ -374,13 +385,9 @@ const createBookmarksSubmenu = (CommonMenu) => { 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))) + CommonMenu.importBookmarksMenuItem(), + CommonMenu.separatorMenuItem + ].concat(menuUtil.createBookmarkMenuItems()) } const createWindowSubmenu = (CommonMenu) => { @@ -497,89 +504,29 @@ const createDebugSubmenu = (CommonMenu) => { } /** - * 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 + * Will only build the initial menu, which is mostly static items + * Dynamic items (Bookmarks, History) get updated w/ updateMenu */ 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('file'), submenu: createFileSubmenu(CommonMenu) }, + { label: locale.translation('edit'), submenu: createEditSubmenu(CommonMenu) }, + { label: locale.translation('view'), submenu: createViewSubmenu(CommonMenu) }, { 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' } + { + label: locale.translation('bravery'), + submenu: [ + CommonMenu.braveryGlobalMenuItem(), + CommonMenu.braverySiteMenuItem() + ] + }, + { label: locale.translation('window'), submenu: createWindowSubmenu(CommonMenu), role: 'window' }, + { label: locale.translation('help'), submenu: createHelpSubmenu(CommonMenu), role: 'help' } ] if (process.env.NODE_ENV === 'development') { - if (!debugSubmenu) { debugSubmenu = createDebugSubmenu(CommonMenu) } - template.push({ label: 'Debug', submenu: debugSubmenu }) + template.push({ label: 'Debug', submenu: createDebugSubmenu(CommonMenu) }) } if (isDarwin) { @@ -625,35 +572,78 @@ const createMenu = (CommonMenu) => { const oldMenu = appMenu appMenu = Menu.buildFromTemplate(template) Menu.setApplicationMenu(appMenu) - oldMenu.destroy() + if (oldMenu) { + oldMenu.destroy() + } +} + +const updateMenu = (CommonMenu, appState, windowData) => { + const updated = menuUtil.checkForUpdate(appState, windowData) + if (updated.nothingUpdated) { + return + } + + // When bookmarks are removed via AppStore (context menu, etc), `isBookmarkChecked` needs to be recalculated + if (windowData && windowData.get('location')) { + isBookmarkChecked = isSiteBookmarked(appState.get('sites'), Immutable.fromJS({location: windowData.get('location')})) + } + + // Only rebuild menus when necessary + + if (updated.settings || updated.closedFrames) { + let historyMenu = menuUtil.getParentMenuDetails(appMenu, locale.translation('history')) + if (historyMenu && historyMenu.menu && historyMenu.menu.submenu && historyMenu.index !== -1) { + const menu = historyMenu.menu.submenu + const menuItems = createHistorySubmenu(CommonMenu) + menu.clear() + menuItems.forEach((item) => menu.append(new MenuItem(item))) + } + } + + if (updated.sites) { + let bookmarksMenu = menuUtil.getParentMenuDetails(appMenu, locale.translation('bookmarks')) + if (bookmarksMenu && bookmarksMenu.menu && bookmarksMenu.menu.submenu && bookmarksMenu.index !== -1) { + const menu = bookmarksMenu.menu.submenu + const menuItems = createBookmarksSubmenu(CommonMenu) + menu.clear() + menuItems.forEach((item) => menu.append(new MenuItem(item))) + } + } + + Menu.setApplicationMenu(appMenu) } /** * Sets up the menu. - * @param {Object} settingsState - Application settings state - * @param {List} - list of siteDetails + * @param {Object} appState - Application state. Used to fetch bookmarks and settings (like homepage) + * @param {Object} windowData - Information specific to the current window (recently closed tabs, etc) */ -const init = (settingsState, sites) => { +module.exports.init = (appState, windowData) => { // 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) + } else { + updateMenu(CommonMenu, appState, windowData) } } -module.exports = { - init, - updateBookmarkedStatus +/** + * Called from navigationBar.js; used to update bookmarks menu status + * @param {boolean} isBookmarked - true if the currently viewed site is bookmarked + */ +module.exports.updateBookmarkedStatus = (isBookmarked) => { + const menuItem = menuUtil.getMenuItem(locale.translation('bookmarkPage')) + if (menuItem) { + menuItem.checked = isBookmarked + } + // menu may be rebuilt without the location changing + // this holds the last known status + isBookmarkChecked = isBookmarked } diff --git a/js/about/entry.js b/js/about/entry.js index 0dd4a66a30d..8891698c559 100644 --- a/js/about/entry.js +++ b/js/about/entry.js @@ -35,6 +35,9 @@ switch (getBaseUrl(getSourceAboutUrl(window.location.href))) { break case 'about:flash': element = require('./flashPlaceholder') + break + case 'about:history': + element = require('./history') } if (element) { diff --git a/js/about/history.js b/js/about/history.js new file mode 100644 index 00000000000..5523bd2e381 --- /dev/null +++ b/js/about/history.js @@ -0,0 +1,128 @@ +/* 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/. */ + +// Note that these are webpack requires, not CommonJS node requiring requires +const React = require('react') +const Immutable = require('immutable') +const Sticky = require('react-stickynode') +const ImmutableComponent = require('../components/immutableComponent') +const messages = require('../constants/messages') +const aboutActions = require('./aboutActions') + +const ipc = window.chrome.ipc + +// Stylesheets +require('../../less/about/itemList.less') +require('../../less/about/history.less') +require('../../node_modules/font-awesome/css/font-awesome.css') + +class HistoryItem extends ImmutableComponent { + navigate () { + aboutActions.newFrame({ + location: this.props.history.get('location'), + partitionNumber: this.props.history.get('partitionNumber') + }) + } + render () { + // Figure out the partition info display + let partitionNumberInfo + if (this.props.history.get('partitionNumber')) { + let l10nArgs = { + partitionNumber: this.props.history.get('partitionNumber') + } + partitionNumberInfo = +  () + } + + var className = 'listItem' + + // If the history item is in the selected folder, show + // it as selected + if (this.props.inSelectedFolder) { + className += ' selected' + } + + return
+ { + this.props.history.get('customTitle') || this.props.history.get('title') + ? + {new Date(this.props.history.get('lastAccessedTime')).toLocaleDateString()} + {this.props.history.get('customTitle') || this.props.history.get('title')} + {partitionNumberInfo} + -{this.props.history.get('location')} + + : + {new Date(this.props.history.get('lastAccessedTime')).toLocaleDateString()} + {this.props.history.get('location')} + {partitionNumberInfo} + + } +
+ } +} + +class HistoryList extends ImmutableComponent { + render () { + return + { + this.props.history.map((entry) => + ) + } + + } +} + +class AboutHistory extends React.Component { + constructor () { + super() + this.onChangeSelectedEntry = this.onChangeSelectedEntry.bind(this) + this.onChangeSearch = this.onChangeSearch.bind(this) + this.onClearSearchText = this.onClearSearchText.bind(this) + this.state = { + history: Immutable.Map(), + selectedEntry: 0, + search: '' + } + ipc.on(messages.HISTORY_UPDATED, (e, detail) => { + this.setState({ + history: Immutable.fromJS(detail && detail.history || {}) + }) + }) + } + onChangeSelectedEntry (id) { + this.setState({ + selectedEntry: id, + search: '' + }) + } + onChangeSearch (evt) { + this.setState({ + search: evt.target.value + }) + } + onClearSearchText (evt) { + this.setState({ + search: '' + }) + } + render () { + return
+

+ +
+ + site.get('tags').isEmpty())} + onChangeSelectedEntry={this.onChangeSelectedEntry} + selectedEntry={this.state.selectedEntry} /> + +
+

+ } +} + +module.exports = diff --git a/js/commonMenu.js b/js/commonMenu.js index c137263fe58..82ea01c4ec0 100644 --- a/js/commonMenu.js +++ b/js/commonMenu.js @@ -13,9 +13,6 @@ 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 { @@ -194,6 +191,22 @@ module.exports.bookmarksManagerMenuItem = () => { } } +module.exports.historyMenuItem = () => { + return { + label: locale.translation('showAllHistory'), + accelerator: 'CmdOrCtrl+Y', + click: function (item, focusedWindow) { + if (BrowserWindow.getAllWindows().length === 0) { + appActions.newWindow(Immutable.fromJS({ + location: 'about:history' + })) + } else { + module.exports.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_NEW_FRAME, 'about:history', { singleFrame: true }]) + } + } + } +} + module.exports.downloadsMenuItem = () => { return { label: locale.translation('downloadsManager'), @@ -253,46 +266,6 @@ 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/frame.js b/js/components/frame.js index 83a217d0a65..146cd6be7d5 100644 --- a/js/components/frame.js +++ b/js/components/frame.js @@ -85,6 +85,10 @@ class Frame extends ImmutableComponent { bookmarks: this.props.bookmarks.toJS(), bookmarkFolders: this.props.bookmarkFolders.toJS() }) + } else if (location === 'about:history') { + this.webview.send(messages.HISTORY_UPDATED, { + history: this.props.history.toJS() + }) } else if (location === 'about:downloads') { this.webview.send(messages.DOWNLOADS_UPDATED, { downloads: this.props.downloads.toJS() diff --git a/js/components/main.js b/js/components/main.js index f3dc402509a..68b55dda0d4 100644 --- a/js/components/main.js +++ b/js/components/main.js @@ -894,6 +894,9 @@ class Main extends ImmutableComponent { .filter((site) => site.get('tags') .includes(siteTags.BOOKMARK)) || emptyMap : null} + history={frame.get('location') === 'about:history' + ? this.props.appState.get('sites') || emptyMap + : null} downloads={this.props.appState.get('downloads') || emptyMap} bookmarkFolders={frame.get('location') === 'about:bookmarks' ? this.props.appState.get('sites') diff --git a/js/components/navigationBar.js b/js/components/navigationBar.js index 555f831f165..630c4595a1d 100644 --- a/js/components/navigationBar.js +++ b/js/components/navigationBar.js @@ -51,8 +51,12 @@ class NavigationBar extends ImmutableComponent { if (!isBookmarked) { appActions.addSite(siteDetail, siteTags.BOOKMARK) } + // Show bookmarks toolbar after first bookmark is saved appActions.changeSetting(settings.SHOW_BOOKMARKS_TOOLBAR, !hasBookmark || showBookmarksToolbar) + // trigger the AddEditBookmark modal windowActions.setBookmarkDetail(siteDetail, siteDetail) + // Update checked/unchecked status in the Bookmarks menu + ipc.send(messages.UPDATE_MENU_BOOKMARKED_STATUS, this.bookmarked) } onReload (e) { @@ -96,8 +100,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}) + // Set initial checked/unchecked status in Bookmarks menu + ipc.send(messages.UPDATE_MENU_BOOKMARKED_STATUS, this.bookmarked) } get showNoScriptInfo () { @@ -118,8 +122,7 @@ class NavigationBar extends ImmutableComponent { })) 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}) + ipc.send(messages.UPDATE_MENU_BOOKMARKED_STATUS, this.bookmarked) } if (this.props.noScriptIsVisible && !this.showNoScriptInfo) { // There are no blocked scripts, so hide the noscript dialog. diff --git a/js/constants/config.js b/js/constants/config.js index 9e57191313b..7d6008a8a84 100644 --- a/js/constants/config.js +++ b/js/constants/config.js @@ -31,7 +31,8 @@ module.exports = { maxTopSites: 5 }, navigationBar: { - defaultSearchSuggestions: false + defaultSearchSuggestions: false, + maxHistorySites: 10 }, defaultOpenSearchPath: 'content/search/google.xml', vault: { diff --git a/js/constants/messages.js b/js/constants/messages.js index 4a3911f770b..40ddef4acdc 100644 --- a/js/constants/messages.js +++ b/js/constants/messages.js @@ -46,7 +46,6 @@ const messages = { QUIT_APPLICATION: _, OPEN_BRAVERY_PANEL: _, PREFS_RESTART: _, - UPDATE_APP_MENU: _, /** @arg {Object} args menu args to update */ CERT_ERROR: _, /** @arg {Object} details of certificate error */ LOGIN_REQUIRED: _, /** @arg {Object} details of the login required request */ LOGIN_RESPONSE: _, @@ -110,6 +109,10 @@ const messages = { RESPONSE_WINDOW_STATE: _, LAST_WINDOW_STATE: _, UNDO_CLOSED_WINDOW: _, + // Menu rebuilding + REQUEST_MENU_DATA_FOR_WINDOW: _, + RESPONSE_MENU_DATA_FOR_WINDOW: _, + UPDATE_MENU_BOOKMARKED_STATUS: _, /** @arg {Object} currently only has a boolean "bookmarked" */ // Ad block, safebrowsing, and tracking protection BLOCKED_RESOURCE: _, BLOCKED_PAGE: _, @@ -118,6 +121,7 @@ const messages = { SITE_SETTINGS_UPDATED: _, BRAVERY_DEFAULTS_UPDATED: _, BOOKMARKS_UPDATED: _, + HISTORY_UPDATED: _, DOWNLOADS_UPDATED: _, FLASH_UPDATED: _, // About pages from contentScript diff --git a/js/contextMenus.js b/js/contextMenus.js index c2dadfdad2e..533aebd70d9 100644 --- a/js/contextMenus.js +++ b/js/contextMenus.js @@ -33,6 +33,7 @@ const {getBase64FromImageUrl} = require('./lib/imageUtil') const urlParse = require('url').parse const eventUtil = require('./lib/eventUtil') const currentWindow = require('../app/renderer/currentWindow') +const config = require('./constants/config') const isDarwin = process.platform === 'darwin' @@ -915,7 +916,6 @@ function mainTemplateInit (nodeProps, frame) { template.push( CommonMenu.separatorMenuItem, { - // TODO: use locale.translate label: passwordManager.get('displayName'), click: (item, focusedWindow) => { if (focusedWindow) { @@ -954,6 +954,8 @@ function onHamburgerMenu (location, e) { function onMainContextMenu (nodeProps, frame, contextMenuType) { if (contextMenuType === 'bookmark' || contextMenuType === 'bookmark-folder') { onBookmarkContextMenu(Immutable.fromJS(nodeProps), Immutable.fromJS({ location: '', title: '', partitionNumber: frame.get('partitionNumber') })) + } else if (contextMenuType === 'history') { + // TODO: add new onHistoryContextMenu() and associated methods. } else if (contextMenuType === 'download') { onDownloadsToolbarContextMenu(nodeProps.downloadId, Immutable.fromJS(nodeProps)) } else { @@ -1053,8 +1055,9 @@ function onMoreBookmarksMenu (activeFrame, allBookmarkItems, overflowItems, e) { function onBackButtonHistoryMenu (activeFrame, history, rect) { const menuTemplate = [] - if (activeFrame && history) { - for (let index = (history.currentIndex - 1); index > -1; index--) { + if (activeFrame && history && history.entries.length > 0) { + const stopIndex = Math.max(((history.currentIndex - config.navigationBar.maxHistorySites) - 1), -1) + for (let index = (history.currentIndex - 1); index > stopIndex; index--) { const url = history.entries[index].url menuTemplate.push({ @@ -1072,6 +1075,17 @@ function onBackButtonHistoryMenu (activeFrame, history, rect) { } }) } + + // Always display "Show History" link + menuTemplate.push( + CommonMenu.separatorMenuItem, + { + label: locale.translation('showAllHistory'), + click: (e, focusedWindow) => { + windowActions.newFrame({ location: 'about:history' }) + windowActions.setContextMenuDetail() + } + }) } windowActions.setContextMenuDetail(Immutable.fromJS({ @@ -1084,8 +1098,9 @@ function onBackButtonHistoryMenu (activeFrame, history, rect) { function onForwardButtonHistoryMenu (activeFrame, history, rect) { const menuTemplate = [] - if (activeFrame && history) { - for (let index = (history.currentIndex + 1); index < history.entries.length; index++) { + if (activeFrame && history && history.entries.length > 0) { + const stopIndex = Math.min(((history.currentIndex + config.navigationBar.maxHistorySites) + 1), history.entries.length) + for (let index = (history.currentIndex + 1); index < stopIndex; index++) { const url = history.entries[index].url menuTemplate.push({ @@ -1103,6 +1118,17 @@ function onForwardButtonHistoryMenu (activeFrame, history, rect) { } }) } + + // Always display "Show History" link + menuTemplate.push( + CommonMenu.separatorMenuItem, + { + label: locale.translation('showAllHistory'), + click: (e, focusedWindow) => { + windowActions.newFrame({ location: 'about:history' }) + windowActions.setContextMenuDetail() + } + }) } windowActions.setContextMenuDetail(Immutable.fromJS({ diff --git a/js/entry.js b/js/entry.js index 87ba4e89050..753706086b5 100644 --- a/js/entry.js +++ b/js/entry.js @@ -53,6 +53,10 @@ ipc.on(messages.REQUEST_WINDOW_STATE, () => { ipc.send(messages.RESPONSE_WINDOW_STATE, windowStore.getState().toJS()) }) +ipc.on(messages.REQUEST_MENU_DATA_FOR_WINDOW, () => { + ipc.send(messages.RESPONSE_MENU_DATA_FOR_WINDOW, windowStore.getState().toJS()) +}) + if (process.env.NODE_ENV === 'test') { window.appStoreRenderer = appStoreRenderer window.windowActions = require('./actions/windowActions') diff --git a/js/lib/appUrlUtil.js b/js/lib/appUrlUtil.js index b002ad8d7b3..da24c34e1ba 100644 --- a/js/lib/appUrlUtil.js +++ b/js/lib/appUrlUtil.js @@ -75,7 +75,7 @@ module.exports.isIntermediateAboutPage = (location) => ['about:safebrowsing', 'about:error', 'about:certerror'].includes(getBaseUrl(location)) module.exports.isNotImplementedAboutPage = (location) => - ['about:config', 'about:history'].includes(getBaseUrl(location)) + ['about:config'].includes(getBaseUrl(location)) module.exports.isNavigatableAboutPage = (location) => !module.exports.isIntermediateAboutPage(location) && !module.exports.isNotImplementedAboutPage(location) && !['about:newtab', 'about:blank', 'about:flash'].includes(getBaseUrl(location)) diff --git a/js/lib/menuUtil.js b/js/lib/menuUtil.js new file mode 100644 index 00000000000..1cc8f896971 --- /dev/null +++ b/js/lib/menuUtil.js @@ -0,0 +1,166 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +'use strict' + +const CommonMenu = require('../commonMenu') +const messages = require('../constants/messages') +const siteTags = require('../constants/siteTags') +const eventUtil = require('./eventUtil') +const siteUtil = require('../state/siteUtil') +const locale = require('../../app/locale') + +// States which can trigger dynamic menus to change +let lastSettingsState, lastSites, lastClosedFrames + +/** + * Get an electron MenuItem object for the PARENT menu (File, Edit, etc) based on its label + * @param {string} label - the text associated with the menu + * NOTE: label may be a localized string + */ +module.exports.getParentMenuDetails = (appMenu, label) => { + let menuIndex = -1 + let menuItem = null + + if (label && appMenu && appMenu.items && appMenu.items.length > 0) { + menuIndex = appMenu.items.findIndex(function (item, index) { + return item && item.label === label + }) + + if (menuIndex !== -1) { + menuItem = appMenu.items[menuIndex] + } + } + + return { + menu: menuItem, + index: menuIndex + } +} + +/** + * Get the an electron MenuItem object from a Menu based on its label + * @param {string} label - the text associated with the menu + * NOTE: label may be a localized string + */ +module.exports.getMenuItem = (appMenu, label) => { + if (appMenu && appMenu.items && appMenu.items.length > 0) { + for (let i = 0; i < appMenu.items.length; i++) { + const menuItem = appMenu.items[i].submenu && appMenu.items[i].submenu.items.find(function (item) { + return item && item.label === label + }) + if (menuItem) return menuItem + } + } + return null +} + +/** + * 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. + * Should only be updated when appState or windowState change (for history or bookmarks) + * NOTE: settingsState is not used directly; it gets used indirectly via getSetting() + * @param {Object} appState - Application state. Used to fetch bookmarks and settings (like homepage) + * @param {Object} windowData - Information specific to the current window (recently closed tabs, etc) + */ +module.exports.checkForUpdate = (appState, windowData) => { + const updated = { + nothing: true, + settings: false, + sites: false, + closedFrames: false + } + + if (appState && appState.get('settings') !== lastSettingsState) { + // Currently only used for the HOMEPAGE value (bound to history menu) + lastSettingsState = appState.get('settings') + updated.nothing = false + updated.settings = true + } + + if (appState && appState.get('sites') !== lastSites) { + lastSites = appState.get('sites') + updated.nothing = false + updated.sites = true + } + + if (windowData && windowData.get('closedFrames') !== lastClosedFrames) { + lastClosedFrames = windowData.get('closedFrames') + updated.nothing = false + updated.closedFrames = true + } + + return updated +} + +const createBookmarkMenuItems = (bookmarks, parentFolderId) => { + const filteredBookmarks = parentFolderId + ? bookmarks.filter((bookmark) => bookmark.get('parentFolderId') === parentFolderId) + : bookmarks.filter((bookmark) => !bookmark.get('parentFolderId')) + + const 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') || site.get('location'), + click: (item, focusedWindow, e) => { + if (eventUtil.isForSecondaryAction(e)) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_NEW_FRAME, site.get('location'), { openInForeground: !!e.shiftKey }]) + } else { + CommonMenu.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 ? createBookmarkMenuItems(bookmarks, folderId) : null + }) + } + }) + return payload +} + +module.exports.createBookmarkMenuItems = () => { + if (lastSites) { + return createBookmarkMenuItems(siteUtil.getBookmarks(lastSites)) + } + return [] +} + +module.exports.createRecentlyClosedMenuItems = () => { + const payload = [] + if (lastClosedFrames && lastClosedFrames.size > 0) { + payload.push( + CommonMenu.separatorMenuItem, + { + label: locale.translation('recentlyClosed'), + enabled: false + }) + + const lastTen = (lastClosedFrames.size < 10) ? lastClosedFrames : lastClosedFrames.slice(-10) + lastTen.forEach((closedFrame) => { + payload.push({ + label: closedFrame.get('title') || closedFrame.get('location'), + click: (item, focusedWindow, e) => { + if (eventUtil.isForSecondaryAction(e)) { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_NEW_FRAME, closedFrame.get('location'), { openInForeground: !!e.shiftKey }]) + } else { + CommonMenu.sendToFocusedWindow(focusedWindow, [messages.SHORTCUT_ACTIVE_FRAME_LOAD_URL, closedFrame.get('location')]) + } + } + }) + }) + } + return payload +} diff --git a/js/stores/appStore.js b/js/stores/appStore.js index b54b60c9fab..064e1d6ed3d 100644 --- a/js/stores/appStore.js +++ b/js/stores/appStore.js @@ -146,6 +146,10 @@ const createWindow = (browserOpts, defaults, frameOpts, windowState) => { mainWindow.setFullScreen(true) } + mainWindow.on('focus', function () { + mainWindow.webContents.send(messages.REQUEST_MENU_DATA_FOR_WINDOW) + }) + mainWindow.on('resize', function (evt) { // the default window size is whatever the last window resize was appActions.setDefaultWindowSize(evt.sender.getSize()) diff --git a/js/stores/windowStore.js b/js/stores/windowStore.js index 75abc2f675e..68e9105e842 100644 --- a/js/stores/windowStore.js +++ b/js/stores/windowStore.js @@ -434,6 +434,9 @@ const doAction = (action) => { activeFrameKey)) let totalOpenTabs = windowState.get('frames').filter((frame) => !frame.get('pinnedLocation')).size + // History menu needs update (since it shows "Recently Closed" items) + ipc.send(messages.RESPONSE_MENU_DATA_FOR_WINDOW, windowState.toJS()) + // If we reach the limit of opened tabs per page while closing tabs, switch to // the active tab's page otherwise the user will hang on empty page if ((totalOpenTabs % getSetting(settings.TABS_PER_PAGE)) === 0) { diff --git a/less/about/history.less b/less/about/history.less new file mode 100644 index 00000000000..bbe9c24260e --- /dev/null +++ b/less/about/history.less @@ -0,0 +1,47 @@ +@import "./itemList.less"; + +.historyPage { + margin: 20px; + + .historyPageContent { + border-top: 1px solid @chromeBorderColor; + display: flex; + + .historyList { + padding-top: 10px; + overflow: hidden; + + .listItem { + display: flex; + height: 1rem; + + .aboutListItem { + align-items: center; + } + + .aboutItemDate { + color: #aaa; + margin-right: 10px; + } + } + } + + .sticky-outer-wrapper { + min-width: 100%; + padding-top: 10px; + } + } +} + +.searchInput { + float: right; + padding: 5px; + margin-top: -35px; +} + +.searchInputClear { + float: right; + padding: 8px; + margin-top: -39px; + color: #999; +} diff --git a/package.json b/package.json index 9ff0f8d3e7f..31b4be6e332 100644 --- a/package.json +++ b/package.json @@ -127,6 +127,7 @@ "less-loader": "^2.2.1", "mkdirp": "^0.5.1", "mocha": "^2.3.4", + "mockery": "^1.7.0", "ncp": "^2.0.0", "node-gyp": "^3.2.1", "node-libs-browser": "^1.0.0", diff --git a/test/unit/braveUnit.js b/test/unit/braveUnit.js index 98a30f160d5..bbd0e636af8 100644 --- a/test/unit/braveUnit.js +++ b/test/unit/braveUnit.js @@ -1,2 +1 @@ require('jsdom-global')() -require('babel-polyfill') diff --git a/test/unit/lib/menuUtilTest.js b/test/unit/lib/menuUtilTest.js new file mode 100644 index 00000000000..c956dcdae1b --- /dev/null +++ b/test/unit/lib/menuUtilTest.js @@ -0,0 +1,279 @@ +/* global describe, before, after, it */ +const siteTags = require('../../../js/constants/siteTags') +const mockery = require('mockery') +const assert = require('assert') +const Immutable = require('immutable') +require('../braveUnit') + +const defaultMenu = { + items: [ + { + label: 'File', + submenu: { + items: [ + { label: 'open', temp: 1 }, + { label: 'quit', temp: 2 } + ] + } + }, + { + label: 'Edit', + submenu: { + items: [ + { label: 'copy', temp: 3 }, + { label: 'paste', temp: 4 } + ] + } + } + ] +} + +describe('menuUtil', function () { + let menuUtil + + before(function () { + // https://github.com/mfncooper/mockery + // TODO: consider moving to braveUnit.js + mockery.enable({ + warnOnReplace: false, + warnOnUnregistered: false, + useCleanCache: true + }) + + const fakeElectron = { + ipcMain: { + on: function () { } + }, + remote: { + app: { } + }, + app: { } + } + + mockery.registerMock('electron', fakeElectron) + menuUtil = require('../../../js/lib/menuUtil') + }) + + after(function () { + mockery.disable() + }) + + describe('getParentMenuDetails', function () { + const emptyValue = { + menu: null, + index: -1 + } + it('returns an object with the electron MenuItem/index based on the label', function () { + const menu = menuUtil.getParentMenuDetails(defaultMenu, 'Edit') + assert.equal(menu.index, 1) + assert.equal(menu.menu, defaultMenu.items[1]) + }) + it('returns an object with null/-1 if input menu is not truthy', function () { + const menu = menuUtil.getParentMenuDetails(null, 'Edit') + assert.deepEqual(menu, emptyValue) + }) + it('returns an object with null/-1 if label is not truthy', function () { + const menu = menuUtil.getParentMenuDetails(defaultMenu, undefined) + assert.deepEqual(menu, emptyValue) + }) + it('returns an object with null/-1 if label is not found', function () { + const menu = menuUtil.getParentMenuDetails(defaultMenu, 'History') + assert.deepEqual(menu, emptyValue) + }) + }) + + describe('getMenuItem', function () { + it('returns the electron MenuItem based on the label', function () { + const menuItem = menuUtil.getMenuItem(defaultMenu, 'quit') + assert.equal(menuItem.temp, 2) + }) + it('returns null if label is not found', function () { + const menuItem = menuUtil.getMenuItem(defaultMenu, 'not-in-here') + assert.equal(menuItem, null) + }) + }) + + describe('checkForUpdate', function () { + const appStateSettings = Immutable.fromJS({ + settings: { + key: 'value' + } + }) + const appStateSites = Immutable.fromJS({ + sites: { + key: 'value' + } + }) + const windowStateClosedFrames = Immutable.fromJS({ + closedFrames: [{ + frameId: 1 + }] + }) + it('sets updated.nothing to false if `settings` was updated', function () { + const updated = menuUtil.checkForUpdate(appStateSettings, null) + assert.equal(updated.nothing, false) + }) + it('sets updated.nothing to false if `sites` was updated', function () { + const updated = menuUtil.checkForUpdate(appStateSites, null) + assert.equal(updated.nothing, false) + console.log(menuUtil.lastSites) + }) + it('sets updated.nothing to false if `closedFrames` was updated', function () { + const updated = menuUtil.checkForUpdate(null, windowStateClosedFrames) + assert.equal(updated.nothing, false) + }) + it('leaves updated.nothing as true if nothing was updated', function () { + const updated = menuUtil.checkForUpdate(null, null) + assert.equal(updated.nothing, true) + }) + it('does not save falsey values', function () { + menuUtil.checkForUpdate(appStateSites, null) + menuUtil.checkForUpdate(null, null) + // NOTE: there should be no change, since last calls values weren't recorded + const updated = menuUtil.checkForUpdate(appStateSites, null) + assert.equal(updated.nothing, true) + }) + }) + + describe('createBookmarkMenuItems', function () { + it('returns an array of items w/ the bookmark tag', function () { + const appStateSites = Immutable.fromJS({ + sites: [ + { tags: [siteTags.BOOKMARK], title: 'my website', location: 'https://brave.com' } + ] + }) + + menuUtil.checkForUpdate(appStateSites, null) + const menuItems = menuUtil.createBookmarkMenuItems() + + assert.equal(Array.isArray(menuItems), true) + assert.equal(menuItems.length, 1) + assert.equal(menuItems[0].label, 'my website') + }) + it('prefers the customTitle field for the bookmark title (over the page title)', function () { + const appStateSites = Immutable.fromJS({ + sites: [ + { tags: [siteTags.BOOKMARK], customTitle: 'use this', title: 'not this', location: 'https://brave.com' } + ] + }) + + menuUtil.checkForUpdate(appStateSites, null) + const menuItems = menuUtil.createBookmarkMenuItems() + + assert.equal(menuItems[0].label, 'use this') + }) + it('only returns bookmarks that have a location set', function () { + const appStateSites = Immutable.fromJS({ + sites: [ + { tags: [siteTags.BOOKMARK], title: 'not valid', location: '' } + ] + }) + + menuUtil.checkForUpdate(appStateSites, null) + const menuItems = menuUtil.createBookmarkMenuItems() + + assert.deepEqual(menuItems, []) + }) + it('returns empty array if no bookmarks present', function () { + const appStateSites = Immutable.fromJS({ + sites: [ + { tags: [], title: 'this is a history entry', location: 'https://brave.com' } + ] + }) + + menuUtil.checkForUpdate(appStateSites, null) + const menuItems = menuUtil.createBookmarkMenuItems() + + assert.deepEqual(menuItems, []) + }) + it('does not count pinned tabs as bookmarks', function () { + const appStateSites = Immutable.fromJS({ + sites: [ + { tags: [siteTags.PINNED], title: 'pinned site', location: 'https://pinned-website.com' }, + { tags: [siteTags.BOOKMARK], title: 'my website', location: 'https://brave.com' } + ] + }) + menuUtil.checkForUpdate(appStateSites, null) + const menuItems = menuUtil.createBookmarkMenuItems() + + assert.equal(menuItems.length, 1) + assert.equal(menuItems[0].label, 'my website') + }) + it('processes folders', function () { + const appStateSites = Immutable.fromJS({ + sites: [ + { tags: [siteTags.BOOKMARK_FOLDER], title: 'my folder', folderId: 123 }, + { tags: [siteTags.BOOKMARK], title: 'my website', location: 'https://brave.com', parentFolderId: 123 } + ] + }) + + menuUtil.checkForUpdate(appStateSites, null) + const menuItems = menuUtil.createBookmarkMenuItems() + + assert.equal(menuItems.length, 1) + assert.equal(menuItems[0].label, 'my folder') + assert.equal(menuItems[0].submenu.length, 1) + assert.equal(menuItems[0].submenu[0].label, 'my website') + }) + it('considers customTitle when processing folders', function () { + const appStateSites = Immutable.fromJS({ + sites: [ + { tags: [siteTags.BOOKMARK_FOLDER], customTitle: 'use this', title: 'not this', folderId: 123 }, + { tags: [siteTags.BOOKMARK], title: 'my website', location: 'https://brave.com', parentFolderId: 123 } + ] + }) + + menuUtil.checkForUpdate(appStateSites, null) + const menuItems = menuUtil.createBookmarkMenuItems() + + assert.equal(menuItems.length, 1) + assert.equal(menuItems[0].label, 'use this') + }) + }) + + describe('createRecentlyClosedMenuItems', function () { + it('returns an array of closedFrames preceded by a separator and "Recently Closed" items', function () { + const windowStateClosedFrames = Immutable.fromJS({ + closedFrames: [{ + title: 'sample', + location: 'https://brave.com' + }] + }) + + menuUtil.checkForUpdate(null, windowStateClosedFrames) + const menuItems = menuUtil.createRecentlyClosedMenuItems() + + assert.equal(Array.isArray(menuItems), true) + assert.equal(menuItems.length, 3) + assert.equal(menuItems[0].type, 'separator') + assert.equal(menuItems[1].label, 'RECENTLYCLOSED') + assert.equal(menuItems[1].enabled, false) + assert.equal(menuItems[2].label, windowStateClosedFrames.get('closedFrames').first().get('title')) + assert.equal(typeof menuItems[2].click === 'function', true) + }) + it('only shows the last 10 items', function () { + const windowStateClosedFrames = Immutable.fromJS({ + closedFrames: [ + { title: 'site01', location: 'https://brave01.com' }, + { title: 'site02', location: 'https://brave02.com' }, + { title: 'site03', location: 'https://brave03.com' }, + { title: 'site04', location: 'https://brave04.com' }, + { title: 'site05', location: 'https://brave05.com' }, + { title: 'site06', location: 'https://brave06.com' }, + { title: 'site07', location: 'https://brave07.com' }, + { title: 'site08', location: 'https://brave08.com' }, + { title: 'site09', location: 'https://brave09.com' }, + { title: 'site10', location: 'https://brave10.com' }, + { title: 'site11', location: 'https://brave11.com' } + ] + }) + + menuUtil.checkForUpdate(null, windowStateClosedFrames) + const menuItems = menuUtil.createRecentlyClosedMenuItems() + + assert.equal(menuItems.length, 12) + assert.equal(menuItems[2].label, windowStateClosedFrames.get('closedFrames').get(1).get('title')) + assert.equal(menuItems[11].label, windowStateClosedFrames.get('closedFrames').get(10).get('title')) + }) + }) +})