diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 2bbc99a10b..73752a792a 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2515,6 +2515,8 @@ 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F132B822B7B00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; 9F982F142B822C7400231028 /* AddEditBookmarkFolderDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */; }; + 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */; }; + 9F9C49F72BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */; }; 9FA173DA2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DB2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; 9FA173DC2B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */; }; @@ -4246,6 +4248,7 @@ 9F872DA22B90920F00138637 /* BookmarkFolderInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFolderInfo.swift; sourceTree = ""; }; 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModel.swift; sourceTree = ""; }; 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogViewModelTests.swift; sourceTree = ""; }; + 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MoreOptionsMenu+BookmarksTests.swift"; sourceTree = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; 9FA173DE2B7A0EFE00EE4E6E /* BookmarkDialogButtonsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogButtonsView.swift; sourceTree = ""; }; 9FA173E22B7A12B600EE4E6E /* BookmarkDialogFolderManagementView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogFolderManagementView.swift; sourceTree = ""; }; @@ -5439,6 +5442,7 @@ 566B195F29CDB7A9007E38F4 /* Mocks */, 378205FA283C277800D1D4AA /* MainMenuTests.swift */, 566B195C29CDB692007E38F4 /* MoreOptionsMenuTests.swift */, + 9F9C49F52BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift */, ); path = Menus; sourceTree = ""; @@ -11206,6 +11210,7 @@ 566B196429CDB824007E38F4 /* MoreOptionsMenuTests.swift in Sources */, 9F0A2CF92B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, 3706FE56293F661700E42796 /* FaviconManagerMock.swift in Sources */, + 9F9C49F72BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 3706FE57293F661700E42796 /* LocalPinningManagerTests.swift in Sources */, 3706FE58293F661700E42796 /* HistoryStoreTests.swift in Sources */, 3706FE59293F661700E42796 /* EncryptionKeyGeneratorTests.swift in Sources */, @@ -13185,6 +13190,7 @@ 85AC3B4925DAC9BD00C7D2AA /* ConfigurationStorageTests.swift in Sources */, 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */, AA91F83927076F1900771A0D /* PrivacyIconViewModelTests.swift in Sources */, + 9F9C49F62BC786790099738D /* MoreOptionsMenu+BookmarksTests.swift in Sources */, 4B723E0726B0003E00E14D75 /* CSVImporterTests.swift in Sources */, 4BCF15EC2ABB9AF80083F6DF /* NetworkProtectionRemoteMessageTests.swift in Sources */, B62EB47C25BAD3BB005745C6 /* WKWebViewPrivateMethodsAvailabilityTests.swift in Sources */, diff --git a/DuckDuckGo/Common/Localizables/UserText.swift b/DuckDuckGo/Common/Localizables/UserText.swift index 4033037606..6a161714e1 100644 --- a/DuckDuckGo/Common/Localizables/UserText.swift +++ b/DuckDuckGo/Common/Localizables/UserText.swift @@ -429,6 +429,7 @@ struct UserText { static let editFavorite = NSLocalizedString("edit.favorite", value: "Edit Favorite", comment: "Header of the view that edits a favorite bookmark") static let removeFromFavorites = NSLocalizedString("remove.from.favorites", value: "Remove from Favorites", comment: "Button for removing bookmarks from favorites") static let bookmarkThisPage = NSLocalizedString("bookmark.this.page", value: "Bookmark This Page", comment: "Menu item for bookmarking current page") + static let bookmarkAllTabs = NSLocalizedString("bookmark.all.tabs", value: "Bookmark All Tabs…", comment: "Menu item for bookmarking all the open tabs") static let bookmarksShowToolbarPanel = NSLocalizedString("bookmarks.show-toolbar-panel", value: "Open Bookmarks Panel", comment: "Menu item for opening the bookmarks panel") static let bookmarksManageBookmarks = NSLocalizedString("bookmarks.manage-bookmarks", value: "Manage Bookmarks", comment: "Menu item for opening the bookmarks management interface") static let bookmarkImportedFromFolder = NSLocalizedString("bookmarks.imported.from.folder", value: "Imported from", comment: "Name of the folder the imported bookmarks are saved into") diff --git a/DuckDuckGo/Localizable.xcstrings b/DuckDuckGo/Localizable.xcstrings index 87e0f9dfb4..761fffcaf8 100644 --- a/DuckDuckGo/Localizable.xcstrings +++ b/DuckDuckGo/Localizable.xcstrings @@ -7429,6 +7429,18 @@ } } }, + "bookmark.all.tabs" : { + "comment" : "Menu item for bookmarking all the open tabs", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Bookmark All Tabs…" + } + } + } + }, "bookmark.dialog.add" : { "comment" : "Button to confim a bookmark creation", "extractionState" : "extracted_with_value", diff --git a/DuckDuckGo/Menus/MainMenu.swift b/DuckDuckGo/Menus/MainMenu.swift index 31d25d3d31..1b6c308664 100644 --- a/DuckDuckGo/Menus/MainMenu.swift +++ b/DuckDuckGo/Menus/MainMenu.swift @@ -294,6 +294,7 @@ import SubscriptionUI func buildBookmarksMenu() -> NSMenuItem { NSMenuItem(title: UserText.bookmarks).submenu(bookmarksMenu.buildItems { NSMenuItem(title: UserText.bookmarkThisPage, action: #selector(MainViewController.bookmarkThisPage), keyEquivalent: "d") + NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(MainViewController.bookmarkAllOpenTabs), keyEquivalent: [.command, .shift, "d"]) manageBookmarksMenuItem bookmarksMenuToggleBookmarksBarMenuItem NSMenuItem.separator() diff --git a/DuckDuckGo/Menus/MainMenuActions.swift b/DuckDuckGo/Menus/MainMenuActions.swift index 171e6699c1..d71a31f3d8 100644 --- a/DuckDuckGo/Menus/MainMenuActions.swift +++ b/DuckDuckGo/Menus/MainMenuActions.swift @@ -507,6 +507,11 @@ extension MainViewController { .openBookmarkPopover(setFavorite: false, accessPoint: .init(sender: sender, default: .moreMenu)) } + @objc func bookmarkAllOpenTabs(_ sender: Any) { + // TODO: https://app.asana.com/0/0/1207032400501907/f + print(#function) + } + @objc func favoriteThisPage(_ sender: Any) { guard let tabIndex = getActiveTabAndIndex()?.index else { return } if tabCollectionViewModel.selectedTabIndex != tabIndex { @@ -947,6 +952,8 @@ extension MainViewController: NSMenuItemValidation { case #selector(MainViewController.bookmarkThisPage(_:)), #selector(MainViewController.favoriteThisPage(_:)): return activeTabViewModel?.canBeBookmarked == true + case #selector(MainViewController.bookmarkAllOpenTabs(_:)): + return tabCollectionViewModel.canBookmarkAllOpenTabs() case #selector(MainViewController.openBookmark(_:)), #selector(MainViewController.showManageBookmarks(_:)): return true diff --git a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift index ed8a2e2b76..7fc5f0ee72 100644 --- a/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift +++ b/DuckDuckGo/NavigationBar/View/MoreOptionsMenu.swift @@ -29,6 +29,7 @@ import Subscription protocol OptionsButtonMenuDelegate: AnyObject { func optionsButtonMenuRequestedBookmarkThisPage(_ sender: NSMenuItem) + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkManagementInterface(_ menu: NSMenu) func optionsButtonMenuRequestedBookmarkImportInterface(_ menu: NSMenu) @@ -174,6 +175,10 @@ final class MoreOptionsMenu: NSMenu { actionDelegate?.optionsButtonMenuRequestedBookmarkThisPage(sender) } + @objc func bookmarkAllOpenTabs(_ sender: NSMenuItem) { + actionDelegate?.optionsButtonMenuRequestedBookmarkAllOpenTabs(sender) + } + @objc func openBookmarks(_ sender: NSMenuItem) { actionDelegate?.optionsButtonMenuRequestedBookmarkPopover(self) } @@ -644,6 +649,12 @@ final class BookmarksSubMenu: NSMenu { bookmarkPageItem.isEnabled = tabCollectionViewModel.selectedTabViewModel?.canBeBookmarked == true + let bookmarkAllTabsItem = addItem(withTitle: UserText.bookmarkAllTabs, action: #selector(MoreOptionsMenu.bookmarkAllOpenTabs(_:)), keyEquivalent: "d") + .withModifierMask([.command, .shift]) + .targetting(target) + + bookmarkAllTabsItem.isEnabled = tabCollectionViewModel.canBookmarkAllOpenTabs() + addItem(NSMenuItem.separator()) addItem(withTitle: UserText.bookmarksShowToolbarPanel, action: #selector(MoreOptionsMenu.openBookmarks(_:)), keyEquivalent: "") diff --git a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift index edd999e760..8c071a6bfd 100644 --- a/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift +++ b/DuckDuckGo/NavigationBar/View/NavigationBarViewController.swift @@ -999,6 +999,11 @@ extension NavigationBarViewController: OptionsButtonMenuDelegate { .openBookmarkPopover(setFavorite: false, accessPoint: .init(sender: sender, default: .moreMenu)) } + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) { + // TODO: https://app.asana.com/0/0/1207032400501907/f + print(#function) + } + func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) { popovers.showBookmarkListPopover(usingView: bookmarkListButton, withDelegate: self, diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 6ebc31e640..2a46a14132 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -1049,6 +1049,15 @@ extension TabBarViewController: TabBarViewItemDelegate { bookmarkTab(with: url, title: tabViewModel.title) } + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool { + tabCollectionViewModel.canBookmarkAllOpenTabs() + } + + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: TabBarViewItem) { + // TODO: https://app.asana.com/0/0/1207032400501907/f + print(#function) + } + func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) { guard let indexPath = collectionView.indexPath(for: tabBarViewItem) else { assertionFailure("TabBarViewController: Failed to get index path of tab bar view item") diff --git a/DuckDuckGo/TabBar/View/TabBarViewItem.swift b/DuckDuckGo/TabBar/View/TabBarViewItem.swift index b4a1bbca64..9e26a41592 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewItem.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewItem.swift @@ -33,6 +33,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemCanBeDuplicated(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBePinned(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: TabBarViewItem) -> Bool func tabBarViewItemCloseAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemTogglePermissionAction(_ tabBarViewItem: TabBarViewItem) @@ -41,6 +42,7 @@ protocol TabBarViewItemDelegate: AnyObject { func tabBarViewItemDuplicateAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemPinAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemBookmarkThisPageAction(_ tabBarViewItem: TabBarViewItem) + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemMoveToNewBurnerWindowAction(_ tabBarViewItem: TabBarViewItem) func tabBarViewItemFireproofSite(_ tabBarViewItem: TabBarViewItem) @@ -196,6 +198,10 @@ final class TabBarViewItem: NSCollectionViewItem { delegate?.tabBarViewItemBookmarkThisPageAction(self) } + @objc func bookmarkAllOpenTabsAction(_ sender: Any) { + delegate?.tabBarViewItemBookmarkAllOpenTabsAction(self) + } + private var lastKnownIndexPath: IndexPath? @IBAction func closeButtonAction(_ sender: Any) { @@ -486,16 +492,19 @@ extension TabBarViewItem: NSMenuDelegate { // Section 1 addDuplicateMenuItem(to: menu) addPinMenuItem(to: menu) - menu.addItem(NSMenuItem.separator()) + addMuteUnmuteMenuItem(to: menu) + menu.addItem(.separator()) // Section 2 - addBookmarkMenuItem(to: menu) addFireproofMenuItem(to: menu) - - addMuteUnmuteMenuItem(to: menu) - menu.addItem(NSMenuItem.separator()) + addBookmarkMenuItem(to: menu) + menu.addItem(.separator()) // Section 3 + addBookmarkAllTabsMenuItem(to: menu) + menu.addItem(.separator()) + + // Section 4 addCloseMenuItem(to: menu) addCloseOtherMenuItem(to: menu, areThereOtherTabs: areThereOtherTabs) addCloseTabsToTheRightMenuItem(to: menu, areThereTabsToTheRight: otherItemsState.hasItemsToTheRight) @@ -525,6 +534,13 @@ extension TabBarViewItem: NSMenuDelegate { menu.addItem(bookmarkMenuItem) } + private func addBookmarkAllTabsMenuItem(to menu: NSMenu) { + let bookmarkMenuItem = NSMenuItem(title: UserText.bookmarkAllTabs, action: #selector(bookmarkAllOpenTabsAction(_:)), keyEquivalent: "") + bookmarkMenuItem.target = self + bookmarkMenuItem.isEnabled = delegate?.tabBarViewAllItemsCanBeBookmarked(self) ?? false + menu.addItem(bookmarkMenuItem) + } + private func addFireproofMenuItem(to menu: NSMenu) { var menuItem = NSMenuItem(title: UserText.fireproofSite, action: #selector(fireproofSiteAction(_:)), keyEquivalent: "") menuItem.isEnabled = false @@ -543,7 +559,6 @@ extension TabBarViewItem: NSMenuDelegate { let audioState = delegate?.tabBarViewItemAudioState(self) ?? .notSupported if audioState != .notSupported { - menu.addItem(NSMenuItem.separator()) let menuItemTitle = audioState == .muted ? UserText.unmuteTab : UserText.muteTab let muteUnmuteMenuItem = NSMenuItem(title: menuItemTitle, action: #selector(muteUnmuteSiteAction(_:)), keyEquivalent: "") muteUnmuteMenuItem.target = self diff --git a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift index e1fd1751c0..675545f99c 100644 --- a/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift +++ b/DuckDuckGo/TabBar/ViewModel/TabCollectionViewModel.swift @@ -762,3 +762,14 @@ extension TabCollectionViewModel { } } + +// MARK: - Bookmark All Open Tabs + +extension TabCollectionViewModel { + + func canBookmarkAllOpenTabs() -> Bool { + // At least two non pinned, non empty (URL only), and not showing an error tabs. + tabViewModels.values.filter(\.canBeBookmarked).count >= 2 + } + +} diff --git a/UnitTests/Menus/MainMenuTests.swift b/UnitTests/Menus/MainMenuTests.swift index 44ec9cdb11..b3b5f4275e 100644 --- a/UnitTests/Menus/MainMenuTests.swift +++ b/UnitTests/Menus/MainMenuTests.swift @@ -18,6 +18,7 @@ import XCTest import Combine +import BrowserServicesKit @testable import DuckDuckGo_Privacy_Browser class MainMenuTests: XCTestCase { @@ -87,4 +88,26 @@ class MainMenuTests: XCTestCase { XCTAssertEqual(manager.reopenLastClosedMenuItem?.keyEquivalent, ReopenMenuItemKeyEquivalentManager.Const.keyEquivalent) XCTAssertEqual(manager.reopenLastClosedMenuItem?.keyEquivalentModifierMask, ReopenMenuItemKeyEquivalentManager.Const.modifierMask) } + + // MARK: - Bookmarks + + @MainActor + func testWhenBookmarksMenuIsInitialized_ThenSecondItemIsBookmarkAllTabs() throws { + // GIVEN + let sut = MainMenu(featureFlagger: DummyFeatureFlagger(), bookmarkManager: MockBookmarkManager(), faviconManager: FaviconManagerMock(), copyHandler: CopyHandler()) + let bookmarksMenu = try XCTUnwrap(sut.item(withTitle: UserText.bookmarks)) + + // WHEN + let result = try XCTUnwrap(bookmarksMenu.submenu?.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertEqual(result.keyEquivalent, "d") + XCTAssertEqual(result.keyEquivalentModifierMask, [.command, .shift]) + } +} + +private class DummyFeatureFlagger: FeatureFlagger { + func isFeatureOn(forProvider: F) -> Bool { + false + } } diff --git a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift index e37582c4fa..05e9c206e7 100644 --- a/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift +++ b/UnitTests/Menus/Mocks/CapturingOptionsButtonMenuDelegate.swift @@ -23,6 +23,7 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { var optionsButtonMenuRequestedPreferencesCalled = false var optionsButtonMenuRequestedAppearancePreferencesCalled = false + var optionsButtonMenuRequestedBookmarkAllOpenTabsCalled = false func optionsButtonMenuRequestedDataBrokerProtection(_ menu: NSMenu) { @@ -36,6 +37,10 @@ class CapturingOptionsButtonMenuDelegate: OptionsButtonMenuDelegate { } + func optionsButtonMenuRequestedBookmarkAllOpenTabs(_ sender: NSMenuItem) { + optionsButtonMenuRequestedBookmarkAllOpenTabsCalled = true + } + func optionsButtonMenuRequestedBookmarkPopover(_ menu: NSMenu) { } diff --git a/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift b/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift new file mode 100644 index 0000000000..3888c658a1 --- /dev/null +++ b/UnitTests/Menus/MoreOptionsMenu+BookmarksTests.swift @@ -0,0 +1,61 @@ +// +// MoreOptionsMenu+BookmarksTests.swift +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class MoreOptionsMenu_BookmarksTests: XCTestCase { + + func testWhenBookmarkSubmenuIsInitThenBookmarkAllTabsKeyIsCmdShiftD() throws { + // GIVEN + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init()) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertEqual(result.keyEquivalent, "d") + XCTAssertEqual(result.keyEquivalentModifierMask, [.command, .shift]) + } + + func testWhenTabCollectionCanBookmarkAllTabsThenBookmarkAllTabsMenuItemIsEnabled() throws { + // GIVEN + let tab1 = Tab(content: .url(.duckDuckGo, credential: nil, source: .ui)) + let tab2 = Tab(content: .url(.duckDuckGoEmail, credential: nil, source: .ui)) + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init(tabCollection: .init(tabs: [tab1, tab2]))) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertTrue(result.isEnabled) + } + + func testWhenTabCollectionCannotBookmarkAllTabsThenBookmarkAllTabsMenuItemIsDisabled() throws { + // GIVEN + let sut = BookmarksSubMenu(targetting: self, tabCollectionViewModel: .init(tabCollection: .init(tabs: []))) + + // WHEN + let result = try XCTUnwrap(sut.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertFalse(result.isEnabled) + } + +} diff --git a/UnitTests/Menus/MoreOptionsMenuTests.swift b/UnitTests/Menus/MoreOptionsMenuTests.swift index 1aec539d0c..9c1c4efc46 100644 --- a/UnitTests/Menus/MoreOptionsMenuTests.swift +++ b/UnitTests/Menus/MoreOptionsMenuTests.swift @@ -162,6 +162,23 @@ final class MoreOptionsMenuTests: XCTestCase { XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedPreferencesCalled) } + // MARK: - Bookmarks + + @MainActor + func testWhenClickingOnBookmarkAllTabsMenuItemThenTheActionDelegateIsAlerted() throws { + // GIVEN + let bookmarksMenu = try XCTUnwrap(moreOptionMenu.item(at: 8)?.submenu) + let bookmarkAllTabsIndex = try XCTUnwrap(bookmarksMenu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) + let bookmarkAllTabsMenuItem = try XCTUnwrap(bookmarksMenu.items[bookmarkAllTabsIndex]) + bookmarkAllTabsMenuItem.isEnabled = true + + // WHEN + bookmarksMenu.performActionForItem(at: bookmarkAllTabsIndex) + + // THEN + XCTAssertTrue(capturingActionDelegate.optionsButtonMenuRequestedBookmarkAllOpenTabsCalled) + } + } final class NetworkProtectionVisibilityMock: NetworkProtectionFeatureVisibility { diff --git a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift index c09b956f23..597c59b9d5 100644 --- a/UnitTests/TabBar/View/MockTabViewItemDelegate.swift +++ b/UnitTests/TabBar/View/MockTabViewItemDelegate.swift @@ -23,9 +23,12 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { var mockedCurrentTab: Tab? + var canBookmarkAllOpenTabs = false var hasItemsToTheRight = false var audioState: WKWebView.AudioState = .notSupported + private(set) var tabBarViewItemBookmarkAllOpenTabsActionCalled = false + func tabBarViewItem(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem, isMouseOver: Bool) { } @@ -70,6 +73,14 @@ class MockTabViewItemDelegate: TabBarViewItemDelegate { } + func tabBarViewAllItemsCanBeBookmarked(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) -> Bool { + canBookmarkAllOpenTabs + } + + func tabBarViewItemBookmarkAllOpenTabsAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { + tabBarViewItemBookmarkAllOpenTabsActionCalled = true + } + func tabBarViewItemMoveToNewWindowAction(_ tabBarViewItem: DuckDuckGo_Privacy_Browser.TabBarViewItem) { } diff --git a/UnitTests/TabBar/View/TabBarViewItemTests.swift b/UnitTests/TabBar/View/TabBarViewItemTests.swift index 77afcb715f..95e12d346d 100644 --- a/UnitTests/TabBar/View/TabBarViewItemTests.swift +++ b/UnitTests/TabBar/View/TabBarViewItemTests.swift @@ -48,31 +48,33 @@ final class TabBarViewItemTests: XCTestCase { XCTAssertEqual(menu.item(at: 0)?.title, UserText.duplicateTab) XCTAssertEqual(menu.item(at: 1)?.title, UserText.pinTab) XCTAssertTrue(menu.item(at: 2)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 3)?.title, UserText.bookmarkThisPage) - XCTAssertEqual(menu.item(at: 4)?.title, UserText.fireproofSite) + XCTAssertEqual(menu.item(at: 3)?.title, UserText.fireproofSite) + XCTAssertEqual(menu.item(at: 4)?.title, UserText.bookmarkThisPage) XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 6)?.title, UserText.closeTab) - XCTAssertEqual(menu.item(at: 7)?.title, UserText.closeOtherTabs) - XCTAssertEqual(menu.item(at: 8)?.title, UserText.closeTabsToTheRight) - XCTAssertEqual(menu.item(at: 9)?.title, UserText.moveTabToNewWindow) + XCTAssertEqual(menu.item(at: 6)?.title, UserText.bookmarkAllTabs) + XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + XCTAssertEqual(menu.item(at: 8)?.title, UserText.closeTab) + XCTAssertEqual(menu.item(at: 9)?.title, UserText.closeOtherTabs) + XCTAssertEqual(menu.item(at: 10)?.title, UserText.closeTabsToTheRight) + XCTAssertEqual(menu.item(at: 11)?.title, UserText.moveTabToNewWindow) } func testThatMuteIsShownWhenCurrentAudioStateIsUnmuted() { delegate.audioState = .unmuted tabBarViewItem.menuNeedsUpdate(menu) - XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 6)?.title, UserText.muteTab) - XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + XCTAssertFalse(menu.item(at: 1)?.isSeparatorItem ?? true) + XCTAssertEqual(menu.item(at: 2)?.title, UserText.muteTab) + XCTAssertTrue(menu.item(at: 3)?.isSeparatorItem ?? false) } func testThatUnmuteIsShownWhenCurrentAudioStateIsMuted() { delegate.audioState = .muted tabBarViewItem.menuNeedsUpdate(menu) - XCTAssertTrue(menu.item(at: 5)?.isSeparatorItem ?? false) - XCTAssertEqual(menu.item(at: 6)?.title, UserText.unmuteTab) - XCTAssertTrue(menu.item(at: 7)?.isSeparatorItem ?? false) + XCTAssertFalse(menu.item(at: 1)?.isSeparatorItem ?? true) + XCTAssertEqual(menu.item(at: 2)?.title, UserText.unmuteTab) + XCTAssertTrue(menu.item(at: 3)?.isSeparatorItem ?? false) } func testWhenOneTabCloseThenOtherTabsItemIsDisabled() { @@ -198,4 +200,42 @@ final class TabBarViewItemTests: XCTestCase { } #endif + func testWhenCanBookmarkAllOpenTabsThenBookmarkAllOpenTabsItemIsEnabled() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = true + tabBarViewItem.menuNeedsUpdate(menu) + + // WHEN + let item = try XCTUnwrap(menu.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertTrue(item.isEnabled) + } + + func testWhenCannotBookmarkAllOpenTabsThenBookmarkAllOpenTabsItemIsDisabled() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = false + tabBarViewItem.menuNeedsUpdate(menu) + + // WHEN + let item = try XCTUnwrap(menu.item(withTitle: UserText.bookmarkAllTabs)) + + // THEN + XCTAssertFalse(item.isEnabled) + } + + func testWhenClickingOnBookmarkAllTabsThenTheActionDelegateIsNotified() throws { + // GIVEN + delegate.canBookmarkAllOpenTabs = true + tabBarViewItem.menuNeedsUpdate(menu) + let index = try XCTUnwrap(menu.indexOfItem(withTitle: UserText.bookmarkAllTabs)) + XCTAssertFalse(delegate.tabBarViewItemBookmarkAllOpenTabsActionCalled) + + // WHEN + menu.performActionForItem(at: index) + + // THEN + XCTAssertTrue(delegate.tabBarViewItemBookmarkAllOpenTabsActionCalled) + } + } diff --git a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift index efc39feac0..3415fd4439 100644 --- a/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift +++ b/UnitTests/TabBar/ViewModel/TabCollectionViewModelTests.swift @@ -425,6 +425,79 @@ final class TabCollectionViewModelTests: XCTestCase { XCTAssertEqual(events.count, 1) XCTAssertIdentical(events[0], tabCollectionViewModel.selectedTabViewModel) } + + // MARK: - Bookmark All Open Tabs + + func testWhenOneEmptyTabOpenThenCanBookmarkAllOpenTabsIsFalse() throws { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + let firstTabViewModel = try XCTUnwrap(sut.tabViewModel(at: 0)) + XCTAssertEqual(sut.tabViewModels.count, 1) + XCTAssertEqual(firstTabViewModel.tabContent, .newtab) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenOneURLTabOpenThenCanBookmarkAllOpenTabsIsFalse() throws { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + sut.replaceTab(at: .unpinned(0), with: .init(content: .url(.duckDuckGo, credential: nil, source: .ui))) + let firstTabViewModel = try XCTUnwrap(sut.tabViewModel(at: 0)) + XCTAssertEqual(sut.tabViewModels.count, 1) + XCTAssertEqual(firstTabViewModel.tabContent, .url(.duckDuckGo, credential: nil, source: .ui)) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenOneURLTabAndOnePinnedTabOpenThenCanBookmarkAllOpenTabsIsFalse() { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + sut.replaceTab(at: .unpinned(0), with: .init(content: .url(.duckDuckGo, credential: nil, source: .ui))) + sut.append(tab: .init(content: .url(.duckDuckGoEmail, credential: nil, source: .ui))) + sut.pinTab(at: 0) + XCTAssertEqual(sut.pinnedTabs.count, 1) + XCTAssertEqual(sut.tabViewModels.count, 1) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertFalse(result) + } + + func testWhenAtLeastTwoURLTabsOpenThenCanBookmarkAllOpenTabsIsTrue() { + // GIVEN + let sut = TabCollectionViewModel.aTabCollectionViewModel() + let pinnedTab = Tab(content: .url(.aboutDuckDuckGo, credential: nil, source: .ui)) + sut.append(tabs: [ + pinnedTab, + .init(content: .url(.duckDuckGo, credential: nil, source: .ui)), + .init(content: .newtab), + .init(content: .bookmarks), + .init(content: .anySettingsPane), + .init(content: .url(.duckDuckGoEmail, credential: nil, source: .ui)), + ]) + sut.pinTab(at: 1) + XCTAssertEqual(sut.pinnedTabs.count, 1) + XCTAssertEqual(sut.tabViewModels.count, 6) + XCTAssertEqual(sut.pinnedTabs.first, pinnedTab) + XCTAssertNil(sut.tabViewModels[pinnedTab]) + + // WHEN + let result = sut.canBookmarkAllOpenTabs() + + // THEN + XCTAssertTrue(result) + } + } fileprivate extension TabCollectionViewModel {