diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 58474a4380..e2a9a80edb 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2399,6 +2399,10 @@ 9F180D102B69C553000D695F /* Tab+WKUIDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */; }; 9F180D122B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; 9F180D132B69C665000D695F /* DownloadsTabExtensionMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */; }; + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */; }; + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */; }; 9F3910622B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910632B68C35600CB5112 /* DownloadsTabExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */; }; 9F3910692B68D87B00CB5112 /* ProgressExtensionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */; }; @@ -2409,6 +2413,9 @@ 9F56CFA92B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; 9F56CFAA2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; 9F56CFAB2B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */; }; + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */; }; 9F982F0D2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0E2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; 9F982F0F2B8224BF00231028 /* AddEditBookmarkFolderDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */; }; @@ -2432,6 +2439,15 @@ 9FDA6C212B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; 9FDA6C232B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */; }; + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */; }; + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */; }; + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */; }; AA06B6B72672AF8100F541C5 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = AA06B6B62672AF8100F541C5 /* Sparkle */; }; AA0877B826D5160D00B05660 /* SafariVersionReaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */; }; AA0877BA26D5161D00B05660 /* WebKitVersionProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */; }; @@ -3954,10 +3970,13 @@ 9DB6E7222AA0DA7A00A17F3C /* LoginItems */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LoginItems; sourceTree = ""; }; 9F180D0E2B69C553000D695F /* Tab+WKUIDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Tab+WKUIDelegateTests.swift"; sourceTree = ""; }; 9F180D112B69C665000D695F /* DownloadsTabExtensionMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionMock.swift; sourceTree = ""; }; + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModelTests.swift; sourceTree = ""; }; + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModelTests.swift; sourceTree = ""; }; 9F3910612B68C35600CB5112 /* DownloadsTabExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsTabExtensionTests.swift; sourceTree = ""; }; 9F3910682B68D87B00CB5112 /* ProgressExtensionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressExtensionTests.swift; sourceTree = ""; }; 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderDialogView.swift; sourceTree = ""; }; 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkFolderView.swift; sourceTree = ""; }; + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogViewModel.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 = ""; }; 9FA173D92B79BD8A00EE4E6E /* BookmarkDialogContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogContainerView.swift; sourceTree = ""; }; @@ -3966,6 +3985,9 @@ 9FA173E62B7B122E00EE4E6E /* BookmarkDialogStackedContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkDialogStackedContentView.swift; sourceTree = ""; }; 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogView.swift; sourceTree = ""; }; 9FDA6C202B79A59D00E099A9 /* BookmarkFavoriteView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkFavoriteView.swift; sourceTree = ""; }; + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkView.swift; sourceTree = ""; }; + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksDialogViewModel.swift; sourceTree = ""; }; + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddEditBookmarkDialogCoordinatorViewModel.swift; sourceTree = ""; }; AA0877B726D5160D00B05660 /* SafariVersionReaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariVersionReaderTests.swift; sourceTree = ""; }; AA0877B926D5161D00B05660 /* WebKitVersionProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebKitVersionProviderTests.swift; sourceTree = ""; }; AA0F3DB6261A566C0077F2D9 /* SuggestionLoadingMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SuggestionLoadingMock.swift; sourceTree = ""; }; @@ -6563,6 +6585,8 @@ isa = PBXGroup; children = ( 9F982F112B82268F00231028 /* AddEditBookmarkFolderDialogViewModelTests.swift */, + 9F2606092B85C20400819292 /* AddEditBookmarkDialogViewModelTests.swift */, + 9F26060D2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift */, ); path = ViewModels; sourceTree = ""; @@ -6578,6 +6602,7 @@ 9FA173EA2B7B232200EE4E6E /* AddEditBookmarkDialogView.swift */, 9F514F902B7D88AD001832A9 /* AddEditBookmarkFolderDialogView.swift */, 9F56CFA82B82DC4300BB7F11 /* AddEditBookmarkFolderView.swift */, + 9FEE98642B846870002E44E8 /* AddEditBookmarkView.swift */, ); path = Dialog; sourceTree = ""; @@ -7334,6 +7359,9 @@ B6F9BDD72B45B7D900677B33 /* AddBookmarkModalViewModel.swift */, B6F9BDDF2B45C1A800677B33 /* AddBookmarkFolderModalViewModel.swift */, 9F982F0C2B8224BE00231028 /* AddEditBookmarkFolderDialogViewModel.swift */, + 9F56CFAC2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift */, + 9FEE98682B85B869002E44E8 /* BookmarksDialogViewModel.swift */, + 9FEE986C2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift */, ); path = ViewModel; sourceTree = ""; @@ -9540,6 +9568,7 @@ 3706FA89293F65D500E42796 /* CrashReportPromptPresenter.swift in Sources */, 3706FA8B293F65D500E42796 /* PreferencesRootView.swift in Sources */, 3706FA8C293F65D500E42796 /* AppStateChangedPublisher.swift in Sources */, + 9FEE986E2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 3706FA8D293F65D500E42796 /* BookmarkTableCellView.swift in Sources */, 3706FA8E293F65D500E42796 /* BookmarkManagementSidebarViewController.swift in Sources */, 3706FA8F293F65D500E42796 /* NSStackViewExtension.swift in Sources */, @@ -9804,6 +9833,7 @@ 3706FB69293F65D500E42796 /* NavigationBarBadgeAnimationView.swift in Sources */, 1D1A334A2A6FEB170080ACED /* BurnerMode.swift in Sources */, B603971B29BA084C00902A34 /* JSAlertController.swift in Sources */, + 9FEE986A2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 3706FB6A293F65D500E42796 /* AddressBarButton.swift in Sources */, 3706FB6B293F65D500E42796 /* HistoryEntry.swift in Sources */, 4B41EDA42B1543B9001EEDF4 /* VPNPreferencesModel.swift in Sources */, @@ -9967,6 +9997,7 @@ 3706FBD9293F65D500E42796 /* NSAppearanceExtension.swift in Sources */, 3706FBDA293F65D500E42796 /* PermissionManager.swift in Sources */, 3706FBDB293F65D500E42796 /* DefaultBrowserPreferences.swift in Sources */, + 9FEE98662B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 3706FBDC293F65D500E42796 /* Permissions.xcdatamodeld in Sources */, 4B41EDB52B168C55001EEDF4 /* VPNFeedbackFormViewModel.swift in Sources */, 3706FBDD293F65D500E42796 /* PaddedImageButton.swift in Sources */, @@ -10003,6 +10034,7 @@ 3706FBF0293F65D500E42796 /* PasswordManagementItemModel.swift in Sources */, 3706FBF2293F65D500E42796 /* FindInPageModel.swift in Sources */, 1D9A4E5B2B43213B00F449E2 /* TabSnapshotExtension.swift in Sources */, + 9F56CFAE2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 3706FBF3293F65D500E42796 /* PseudoFolder.swift in Sources */, 1D26EBAD2B74BECB0002A93F /* NSImageSendable.swift in Sources */, 3706FBF4293F65D500E42796 /* Visit.swift in Sources */, @@ -10254,6 +10286,7 @@ 3706FDDE293F661700E42796 /* SuggestionViewModelTests.swift in Sources */, 3706FDDF293F661700E42796 /* BookmarkSidebarTreeControllerTests.swift in Sources */, 3706FDE0293F661700E42796 /* TabIndexTests.swift in Sources */, + 9F26060F2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 3706FDE1293F661700E42796 /* AdjacentItemEnumeratorTests.swift in Sources */, 3706FDE2293F661700E42796 /* PixelArgumentsTests.swift in Sources */, 4B9DB0572A983B55000927DB /* MockNotificationService.swift in Sources */, @@ -10343,6 +10376,7 @@ 3706FE26293F661700E42796 /* TemporaryFileCreator.swift in Sources */, 3706FE27293F661700E42796 /* AppPrivacyConfigurationTests.swift in Sources */, B626A7652992506A00053070 /* SerpHeadersNavigationResponderTests.swift in Sources */, + 9F26060C2B85C20B00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, 3706FE28293F661700E42796 /* BookmarkTests.swift in Sources */, 3706FE29293F661700E42796 /* SuggestionContainerViewModelTests.swift in Sources */, 1D8C2FEB2B70F5A7005E4BBD /* MockWebViewSnapshotRenderer.swift in Sources */, @@ -10732,6 +10766,7 @@ 4B95796E2AC7AE700062CA31 /* LegacyBookmarkStore.swift in Sources */, 4B95796F2AC7AE700062CA31 /* NSAlert+DataImport.swift in Sources */, 4B9579702AC7AE700062CA31 /* MainWindow.swift in Sources */, + 9FEE986B2B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, 4B9579712AC7AE700062CA31 /* CrashReportPromptViewController.swift in Sources */, 4B9579722AC7AE700062CA31 /* BookmarksCleanupErrorHandling.swift in Sources */, 4B9579732AC7AE700062CA31 /* ContextMenuManager.swift in Sources */, @@ -10859,6 +10894,7 @@ 4B9579E42AC7AE700062CA31 /* PopUpWindow.swift in Sources */, 4B9579E52AC7AE700062CA31 /* Favicons.xcdatamodeld in Sources */, 4B9579E62AC7AE700062CA31 /* Publisher.asVoid.swift in Sources */, + 9FEE986F2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, 4B9579E72AC7AE700062CA31 /* Waitlist.swift in Sources */, 3158B1582B0BF76000AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 4B9579E82AC7AE700062CA31 /* NavigationButtonMenuDelegate.swift in Sources */, @@ -10943,6 +10979,7 @@ 4B957A332AC7AE700062CA31 /* Favicon.swift in Sources */, 1E2AE4CA2ACB21A000684E0A /* NetworkProtectionRemoteMessage.swift in Sources */, 4B957A342AC7AE700062CA31 /* SuggestionContainerViewModel.swift in Sources */, + 9F56CFAF2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, 4B957A352AC7AE700062CA31 /* FirePopoverWrapperViewController.swift in Sources */, 4B957A362AC7AE700062CA31 /* NSPasteboardItemExtension.swift in Sources */, 4B957A372AC7AE700062CA31 /* AutofillPreferencesModel.swift in Sources */, @@ -11091,6 +11128,7 @@ 4B957AB62AC7AE700062CA31 /* FireproofDomains.xcdatamodeld in Sources */, 3158B14F2B0BF74F00AF130C /* DataBrokerProtectionManager.swift in Sources */, 4B957AB82AC7AE700062CA31 /* HomePageView.swift in Sources */, + 9FEE98672B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 4B957AB92AC7AE700062CA31 /* SerpHeadersNavigationResponder.swift in Sources */, 4B957ABA2AC7AE700062CA31 /* HomePageContinueSetUpModel.swift in Sources */, 4B957ABB2AC7AE700062CA31 /* WebKitDownloadTask.swift in Sources */, @@ -11688,6 +11726,7 @@ 85C6A29625CC1FFD00EEB5F1 /* UserDefaultsWrapper.swift in Sources */, 85625998269C9C5F00EE44BC /* PasswordManagementPopover.swift in Sources */, 1DDF076328F815AD00EDFBE3 /* BWCommunicator.swift in Sources */, + 9FEE98652B846870002E44E8 /* AddEditBookmarkView.swift in Sources */, 85589E9127BFB9810038AD11 /* HomePageRecentlyVisitedModel.swift in Sources */, 85012B0229133F9F003D0DCC /* NavigationBarPopovers.swift in Sources */, B626A7602992407D00053070 /* CancellableExtension.swift in Sources */, @@ -11809,6 +11848,7 @@ 3171D6BA288984D00068632A /* BadgeAnimationView.swift in Sources */, 1DB67F292B6FE4A6003DF243 /* WebViewSnapshotRenderer.swift in Sources */, 4B9292CE2667123700AD2C21 /* BrowserTabSelectionDelegate.swift in Sources */, + 9FEE986D2B85BA17002E44E8 /* AddEditBookmarkDialogCoordinatorViewModel.swift in Sources */, B6BCC53B2AFD15DF002C5499 /* DataImportProfilePicker.swift in Sources */, 3158B1562B0BF75D00AF130C /* DataBrokerProtectionFeatureVisibility.swift in Sources */, 56D6A3D629DB2BAB0055215A /* ContinueSetUpView.swift in Sources */, @@ -11906,6 +11946,7 @@ 4BE4005527CF3F19007D3161 /* SavePaymentMethodViewController.swift in Sources */, 1D2DC009290167A0008083A1 /* BWStatus.swift in Sources */, AAFE068326C7082D005434CC /* WebKitVersionProvider.swift in Sources */, + 9FEE98692B85B869002E44E8 /* BookmarksDialogViewModel.swift in Sources */, B63D467A25BFC3E100874977 /* NSCoderExtensions.swift in Sources */, 1D2DC00B290167EC008083A1 /* RunningApplicationCheck.swift in Sources */, B6A5A27125B9377300AA7ADA /* StatePersistenceService.swift in Sources */, @@ -12084,6 +12125,7 @@ 37445F9C2A1569F00029F789 /* SyncBookmarksAdapter.swift in Sources */, B6AAAC3E26048F690029438D /* RandomAccessCollectionExtension.swift in Sources */, 4B9292AF26670F5300AD2C21 /* NSOutlineViewExtensions.swift in Sources */, + 9F56CFAD2B84326C00BB7F11 /* AddEditBookmarkDialogViewModel.swift in Sources */, AA585D82248FD31100E9A3E2 /* AppDelegate.swift in Sources */, 7B1E81A027C8874900FF0E60 /* ContentOverlayViewController.swift in Sources */, B687B7CA2947A029001DEA6F /* ContentBlockingTabExtension.swift in Sources */, @@ -12258,6 +12300,7 @@ 4B8AD0B127A86D9200AE44D6 /* WKWebsiteDataStoreExtensionTests.swift in Sources */, B69B50472726C5C200758A2B /* VariantManagerTests.swift in Sources */, 8546DE6225C03056000CA5E1 /* UserAgentTests.swift in Sources */, + 9F26060B2B85C20A00819292 /* AddEditBookmarkDialogViewModelTests.swift in Sources */, B63ED0DE26AFD9A300A9DAD1 /* AVCaptureDeviceMock.swift in Sources */, 98A95D88299A2DF900B9B81A /* BookmarkMigrationTests.swift in Sources */, B63ED0E026AFE32F00A9DAD1 /* GeolocationProviderMock.swift in Sources */, @@ -12359,6 +12402,7 @@ 4BBF0925283083EC00EE1418 /* FileSystemDSLTests.swift in Sources */, 4B11060A25903EAC0039B979 /* CoreDataEncryptionTests.swift in Sources */, B603971029B9D67E00902A34 /* PublishersExtensions.swift in Sources */, + 9F26060E2B85E17D00819292 /* AddEditBookmarkDialogCoordinatorViewModelTests.swift in Sources */, 4B9292C32667103100AD2C21 /* PasteboardBookmarkTests.swift in Sources */, B610F2E427A8F37A00FCEBE9 /* CBRCompileTimeReporterTests.swift in Sources */, AABAF59C260A7D130085060C /* FaviconManagerMock.swift in Sources */, diff --git a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift index d098e434e8..2ab57158a9 100644 --- a/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift +++ b/DuckDuckGo/Bookmarks/Services/BookmarkStoreMock.swift @@ -49,6 +49,7 @@ public final class BookmarkStoreMock: BookmarkStore { var capturedFolder: BookmarkFolder? var capturedParentFolder: BookmarkFolder? var capturedParentFolderType: ParentFolderType? + var capturedBookmark: Bookmark? var loadAllCalled = false var bookmarks: [BaseBookmarkEntity]? @@ -64,6 +65,8 @@ public final class BookmarkStoreMock: BookmarkStore { func save(bookmark: Bookmark, parent: BookmarkFolder?, index: Int?, completion: @escaping (Bool, Error?) -> Void) { saveBookmarkCalled = true bookmarks?.append(bookmark) + capturedParentFolder = parent + capturedBookmark = bookmark completion(saveBookmarkSuccess, saveBookmarkError) } @@ -98,6 +101,7 @@ public final class BookmarkStoreMock: BookmarkStore { } updateBookmarkCalled = true + capturedBookmark = bookmark } var updateFolderCalled = false diff --git a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift index 2582b61691..8ddb38ce2c 100644 --- a/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift +++ b/DuckDuckGo/Bookmarks/View/AddBookmarkPopoverView.swift @@ -38,44 +38,22 @@ struct AddBookmarkPopoverView: View { @MainActor private var addBookmarkView: some View { - BookmarkDialogContainerView( + AddEditBookmarkView( title: UserText.Bookmarks.Dialog.Title.addedBookmark, - middleSection: { - BookmarkDialogStackedContentView( - .init( - title: UserText.Bookmarks.Dialog.Field.name, - content: TextField("", text: $model.bookmarkTitle) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.location, - content: BookmarkDialogFolderManagementView( - folders: model.folders, - selectedFolder: $model.selectedFolder, - onActionButton: model.addFolderButtonAction - ) - ) - ) - BookmarkFavoriteView(isFavorite: $model.isBookmarkFavorite) - }, - bottomSection: { - BookmarkDialogButtonsView( - viewState: .expanded, - otherButtonAction: .init( - title: UserText.remove, - action: model.removeButtonAction - ), - defaultButtonAction: .init( - title: UserText.done, - keyboardShortCut: .defaultAction, - isDisabled: model.isDefaultActionButtonDisabled, - action: model.doneButtonAction - ) - ) - } + buttonsState: .compressed, + bookmarkName: $model.bookmarkTitle, + bookmarkURLPath: nil, + isBookmarkFavorite: $model.isBookmarkFavorite, + folders: model.folders, + selectedFolder: $model.selectedFolder, + isURLFieldHidden: true, + addFolderAction: model.addFolderButtonAction, + otherActionTitle: UserText.remove, + isOtherActionDisabled: false, + otherAction: model.removeButtonAction, + defaultActionTitle: UserText.done, + isDefaultActionDisabled: model.isDefaultActionButtonDisabled, + defaultAction: model.doneButtonAction ) .padding(.vertical, 16.0) .font(.system(size: 13)) @@ -112,6 +90,7 @@ struct AddBookmarkPopoverView: View { let bkm = Bookmark(id: "n", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false, parentFolderUUID: "1") let bkman = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [ BookmarkFolder(id: "1", title: "Folder with a name that shouldn‘t fit into the picker", children: [])])) + bkman.loadBookmarks() return AddBookmarkPopoverView(model: AddBookmarkPopoverViewModel(bookmark: bkm, bookmarkManager: bkman)) .preferredColorScheme(.dark) diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift index 6d478c5527..103a49ed43 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkDialogView.swift @@ -17,158 +17,115 @@ // import SwiftUI -import Combine -import Common - -@MainActor -final class AddEditBookmarkDialogViewModel: ObservableObject { - - enum Mode { - case add - case edit - } - - @Published var bookmarkName: String - @Published var bookmarkURLPath: String - @Published var isBookmarkFavorite: Bool - @Published var folders: [FolderViewModel] = [] - @Published var selectedFolder: BookmarkFolder? - - let title: String = "" - let defaultActionTitle = "" - var isDefaultActionButtonDisabled: Bool { false } - - private let bookmark: Bookmark - private let mode: Mode - private let bookmarkManager: BookmarkManager - - init( - mode: Mode, - bookmark: Bookmark, - bookmarkManager: LocalBookmarkManager = .shared - ) { - self.bookmark = bookmark - self.mode = mode - self.bookmarkManager = bookmarkManager - bookmarkName = bookmark.title - bookmarkURLPath = bookmark.url - isBookmarkFavorite = bookmark.isFavorite - } - - func cancelAction(dismiss: () -> Void) {} - func saveOrAddAction(dismiss: () -> Void) {} - func addFolderButtonAction() {} -} struct AddEditBookmarkDialogView: ModalView { - @ObservedObject private var viewModel: AddEditBookmarkDialogViewModel + @ObservedObject private var viewModel: AddEditBookmarkDialogCoordinatorViewModel - init(viewModel: AddEditBookmarkDialogViewModel) { + init(viewModel: AddEditBookmarkDialogCoordinatorViewModel) { self.viewModel = viewModel } var body: some View { - BookmarkDialogContainerView( - title: viewModel.title, - middleSection: { - BookmarkDialogStackedContentView( - .init( - title: UserText.Bookmarks.Dialog.Field.name, - content: TextField("", text: $viewModel.bookmarkName) - .focusedOnAppear() - .accessibilityIdentifier("bookmark.add.name.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.url, - content: TextField("", text: $viewModel.bookmarkURLPath) - .accessibilityIdentifier("bookmark.add.url.textfield") - .textFieldStyle(RoundedBorderTextFieldStyle()) - .font(.system(size: 14)) - ), - .init( - title: UserText.Bookmarks.Dialog.Field.location, - content: BookmarkDialogFolderManagementView( - folders: viewModel.folders, - selectedFolder: $viewModel.selectedFolder, - onActionButton: viewModel.addFolderButtonAction - ) - ) - ) - BookmarkFavoriteView(isFavorite: $viewModel.isBookmarkFavorite) - }, - bottomSection: { - BookmarkDialogButtonsView( - viewState: .compressed, - otherButtonAction: .init( - title: UserText.cancel, - keyboardShortCut: .cancelAction, - action: viewModel.cancelAction - ), defaultButtonAction: .init( - title: viewModel.defaultActionTitle, - keyboardShortCut: .defaultAction, - isDisabled: viewModel.isDefaultActionButtonDisabled, - action: viewModel.saveOrAddAction - ) - ) + Group { + switch viewModel.viewState { + case .bookmark: + addEditBookmarkView + case .folder: + addFolderView } - ) + } .font(.system(size: 13)) - .frame(width: 448, height: 288) } -} - -// MARK: - AddEditBookmarkDialogViewModel.Mode -private extension AddEditBookmarkDialogViewModel.Mode { - - var title: String { - switch self { - case .add: - return UserText.Bookmarks.Dialog.Title.addBookmark - case .edit: - return UserText.Bookmarks.Dialog.Title.editBookmark - } + private var addEditBookmarkView: some View { + AddEditBookmarkView( + title: viewModel.bookmarkModel.title, + buttonsState: .compressed, + bookmarkName: $viewModel.bookmarkModel.bookmarkName, + bookmarkURLPath: $viewModel.bookmarkModel.bookmarkURLPath, + isBookmarkFavorite: $viewModel.bookmarkModel.isBookmarkFavorite, + folders: viewModel.bookmarkModel.folders, + selectedFolder: $viewModel.bookmarkModel.selectedFolder, + isURLFieldHidden: false, + addFolderAction: viewModel.addFolderAction, + otherActionTitle: viewModel.bookmarkModel.cancelActionTitle, + isOtherActionDisabled: viewModel.bookmarkModel.isOtherActionDisabled, + otherAction: viewModel.bookmarkModel.cancel, + defaultActionTitle: viewModel.bookmarkModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.bookmarkModel.isDefaultActionDisabled, + defaultAction: viewModel.bookmarkModel.addOrSave + ) + .frame(width: 448, height: 288) } - var defaultActionTitle: String { - switch self { - case .add: - return UserText.Bookmarks.Dialog.Action.addBookmark - case .edit: - return UserText.save - } + private var addFolderView: some View { + AddEditBookmarkFolderView( + title: viewModel.folderModel.title, + buttonsState: .compressed, + folders: viewModel.folderModel.folders, + folderName: $viewModel.folderModel.folderName, + selectedFolder: $viewModel.folderModel.selectedFolder, + cancelActionTitle: viewModel.folderModel.cancelActionTitle, + isCancelActionDisabled: viewModel.folderModel.isOtherActionDisabled, + cancelAction: { _ in + viewModel.dismissAction() + }, + defaultActionTitle: viewModel.folderModel.defaultActionTitle, + isDefaultActionDisabled: viewModel.folderModel.isDefaultActionDisabled, + defaultAction: { _ in + viewModel.folderModel.addOrSave { + viewModel.dismissAction() + } + } + ) + .frame(width: 448, height: 210) } - } // MARK: - Previews +#if DEBUG #Preview("Add Bookmark - Light Mode") { - let bookmark = Bookmark(id: "1", url: "", title: "", isFavorite: false) - let viewModel = AddEditBookmarkDialogViewModel(mode: .add, bookmark: bookmark) + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) .preferredColorScheme(.light) } -#Preview("Edit Bookmark - Light Mode") { - let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true) - let viewModel = AddEditBookmarkDialogViewModel(mode: .edit, bookmark: bookmark) +#Preview("Add Bookmark - Light Mode") { + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) - .preferredColorScheme(.light) + .preferredColorScheme(.dark) } -#Preview("Add Bookmark - Dark Mode") { - let bookmark = Bookmark(id: "1", url: "", title: "", isFavorite: false) - let viewModel = AddEditBookmarkDialogViewModel(mode: .add, bookmark: bookmark) +#Preview("Edit Bookmark - Light Mode") { + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) - .preferredColorScheme(.dark) + .preferredColorScheme(.light) } #Preview("Edit Bookmark - Dark Mode") { - let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true) - let viewModel = AddEditBookmarkDialogViewModel(mode: .edit, bookmark: bookmark) + let parentFolder = BookmarkFolder(id: "7", title: "DuckDuckGo") + let bookmark = Bookmark(id: "1", url: "www.duckduckgo.com", title: "DuckDuckGo", isFavorite: true, parentFolderUUID: "7") + let bookmarkManager = LocalBookmarkManager(bookmarkStore: BookmarkStoreMock(bookmarks: [bookmark, parentFolder])) + bookmarkManager.loadBookmarks() + let bookmarkViewModel = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + let folderViewModel = AddEditBookmarkFolderDialogViewModel(mode: .add(parentFolder: nil), bookmarkManager: bookmarkManager) + let viewModel = AddEditBookmarkDialogCoordinatorViewModel(bookmarkModel: bookmarkViewModel, folderModel: folderViewModel) return AddEditBookmarkDialogView(viewModel: viewModel) .preferredColorScheme(.dark) } +#endif diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift index ff8f9b827d..f1a9174fa9 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkFolderDialogView.swift @@ -33,10 +33,10 @@ struct AddEditBookmarkFolderDialogView: ModalView { folderName: $viewModel.folderName, selectedFolder: $viewModel.selectedFolder, cancelActionTitle: viewModel.cancelActionTitle, - isCancelActionDisabled: viewModel.isCancelActionDisabled, + isCancelActionDisabled: viewModel.isOtherActionDisabled, cancelAction: viewModel.cancel, defaultActionTitle: viewModel.defaultActionTitle, - isDefaultActionDisabled: viewModel.isDefaultActionButtonDisabled, + isDefaultActionDisabled: viewModel.isDefaultActionDisabled, defaultAction: viewModel.addOrSave ) .font(.system(size: 13)) diff --git a/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift new file mode 100644 index 0000000000..8d34889432 --- /dev/null +++ b/DuckDuckGo/Bookmarks/View/Dialog/AddEditBookmarkView.swift @@ -0,0 +1,113 @@ +// +// AddEditBookmarkView.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 SwiftUI + +struct AddEditBookmarkView: View { + let title: String + let buttonsState: BookmarksDialogButtonsState + + @Binding var bookmarkName: String + var bookmarkURLPath: Binding? + @Binding var isBookmarkFavorite: Bool + + let folders: [FolderViewModel] + @Binding var selectedFolder: BookmarkFolder? + + let isURLFieldHidden: Bool + + let addFolderAction: () -> Void + + let otherActionTitle: String + let isOtherActionDisabled: Bool + let otherAction: @MainActor (_ dismiss: () -> Void) -> Void + + let defaultActionTitle: String + let isDefaultActionDisabled: Bool + let defaultAction: @MainActor (_ dismiss: () -> Void) -> Void + + var body: some View { + BookmarkDialogContainerView( + title: title, + middleSection: { + BookmarkDialogStackedContentView( + .init( + title: UserText.Bookmarks.Dialog.Field.name, + content: TextField("", text: $bookmarkName) + .focusedOnAppear() + .accessibilityIdentifier("bookmark.add.name.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)) + ), + .init( + title: UserText.Bookmarks.Dialog.Field.url, + content: TextField("", text: bookmarkURLPath ?? .constant("")) + .accessibilityIdentifier("bookmark.add.url.textfield") + .textFieldStyle(RoundedBorderTextFieldStyle()) + .font(.system(size: 14)), + isContentViewHidden: isURLFieldHidden + ), + .init( + title: UserText.Bookmarks.Dialog.Field.location, + content: BookmarkDialogFolderManagementView( + folders: folders, + selectedFolder: $selectedFolder, + onActionButton: addFolderAction + ) + ) + ) + BookmarkFavoriteView(isFavorite: $isBookmarkFavorite) + }, + bottomSection: { + BookmarkDialogButtonsView( + viewState: .init(buttonsState), + otherButtonAction: .init( + title: otherActionTitle, + isDisabled: isOtherActionDisabled, + action: otherAction + ), + defaultButtonAction: .init( + title: defaultActionTitle, + keyboardShortCut: .defaultAction, + isDisabled: isDefaultActionDisabled, + action: defaultAction + ) + ) + } + ) + } + +} + +// MARK: - BookmarksDialogButtonsState + +enum BookmarksDialogButtonsState { + case compressed + case expanded +} + +extension BookmarkDialogButtonsView.ViewState { + init(_ state: BookmarksDialogButtonsState) { + switch state { + case .compressed: + self = .compressed + case .expanded: + self = .expanded + } + } +} diff --git a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift index df63a94090..864b0cdb13 100644 --- a/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift +++ b/DuckDuckGo/Bookmarks/View/Dialog/BookmarkDialogStackedContentView.swift @@ -37,14 +37,18 @@ struct BookmarkDialogStackedContentView: View { rowHeight: 22.0, leftColumn: { ForEach(items, id: \.title) { item in - Text(item.title) - .foregroundColor(.primary) - .fontWeight(.medium) + if !item.isContentViewHidden { + Text(item.title) + .foregroundColor(.primary) + .fontWeight(.medium) + } } }, rightColumn: { ForEach(items, id: \.title) { item in - item.content + if !item.isContentViewHidden { + item.content + } } } ) @@ -57,10 +61,12 @@ extension BookmarkDialogStackedContentView { struct Item { fileprivate let title: String fileprivate let content: AnyView + fileprivate let isContentViewHidden: Bool - init(title: String, content: any View) { + init(title: String, content: any View, isContentViewHidden: Bool = false) { self.title = title self.content = AnyView(content) + self.isContentViewHidden = isContentViewHidden } } } diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift new file mode 100644 index 0000000000..27fc0c64e5 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogCoordinatorViewModel.swift @@ -0,0 +1,74 @@ +// +// AddEditBookmarkDialogCoordinatorViewModel.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 SwiftUI +import Combine + +final class AddEditBookmarkDialogCoordinatorViewModel: ObservableObject { + @ObservedObject var bookmarkModel: BookmarkViewModel + @ObservedObject var folderModel: AddFolderViewModel + @Published var viewState: ViewState + + private var cancellables: Set = [] + + init(bookmarkModel: BookmarkViewModel, folderModel: AddFolderViewModel) { + self.bookmarkModel = bookmarkModel + self.folderModel = folderModel + viewState = .bookmark + bind() + } + + func dismissAction() { + viewState = .bookmark + } + + func addFolderAction() { + folderModel.selectedFolder = bookmarkModel.selectedFolder + viewState = .folder + } + + private func bind() { + bookmarkModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.objectWillChange + .receive(on: DispatchQueue.main) + .sink { [weak self] _ in + self?.objectWillChange.send() + } + .store(in: &cancellables) + + folderModel.addFolderPublisher + .receive(on: DispatchQueue.main) + .sink { [weak self] bookmarkFolder in + self?.bookmarkModel.selectedFolder = bookmarkFolder + } + .store(in: &cancellables) + } +} + +extension AddEditBookmarkDialogCoordinatorViewModel { + enum ViewState { + case bookmark + case folder + } +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift new file mode 100644 index 0000000000..b511c17961 --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkDialogViewModel.swift @@ -0,0 +1,182 @@ +// +// AddEditBookmarkDialogViewModel.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 Foundation +import Combine + +@MainActor +protocol BookmarkDialogEditing: BookmarksDialogViewModel { + var bookmarkName: String { get set } + var bookmarkURLPath: String { get set } + var isBookmarkFavorite: Bool { get set } + + var isURLFieldHidden: Bool { get } +} + +@MainActor +final class AddEditBookmarkDialogViewModel: BookmarkDialogEditing { + + enum Mode { + case add(parentFolder: BookmarkFolder? = nil) + case edit(bookmark: Bookmark) + } + + @Published var bookmarkName: String + @Published var bookmarkURLPath: String + @Published var isBookmarkFavorite: Bool + + @Published private(set) var folders: [FolderViewModel] + @Published var selectedFolder: BookmarkFolder? + + private var folderCancellable: AnyCancellable? + + var title: String { + mode.title + } + + let isURLFieldHidden: Bool = false + + var cancelActionTitle: String { + mode.cancelActionTitle + } + + var defaultActionTitle: String { + mode.defaultActionTitle + } + + private var hasValidInput: Bool { + guard let url = bookmarkURLPath.url else { return false } + return !bookmarkName.trimmingWhitespace().isEmpty && url.isValid + } + + let isOtherActionDisabled: Bool = false + + var isDefaultActionDisabled: Bool { !hasValidInput } + + private let mode: Mode + private let bookmarkManager: BookmarkManager + + init(mode: Mode, bookmarkManager: LocalBookmarkManager = .shared) { + self.mode = mode + self.bookmarkManager = bookmarkManager + bookmarkName = mode.bookmark?.title ?? "" + bookmarkURLPath = mode.bookmark?.url ?? "" + isBookmarkFavorite = mode.bookmark?.isFavorite ?? false + folders = .init(bookmarkManager.list) + switch mode { + case let .add(parentFolder): + selectedFolder = parentFolder + case let .edit(bookmark): + selectedFolder = folders.first(where: { $0.id == bookmark.parentFolderUUID })?.entity + } + bind() + } + + func cancel(dismiss: () -> Void) { + dismiss() + } + + func addOrSave(dismiss: () -> Void) { + guard let url = bookmarkURLPath.url else { + assertionFailure("Invalid URL, default action button should be disabled.") + return + } + + let trimmedBookmarkName = bookmarkName.trimmingWhitespace() + + switch mode { + case .add: + addBookmark(withURL: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, to: selectedFolder) + case let .edit(bookmark): + updateBookmark(bookmark, url: url, name: trimmedBookmarkName, isFavorite: isBookmarkFavorite, location: selectedFolder) + } + dismiss() + } +} + +private extension AddEditBookmarkDialogViewModel { + + func bind() { + folderCancellable = bookmarkManager.listPublisher + .receive(on: DispatchQueue.main) + .sink(receiveValue: { bookmarkList in + self.folders = .init(bookmarkList) + }) + } + + func updateBookmark(_ bookmark: Bookmark, url: URL, name: String, isFavorite: Bool, location: BookmarkFolder?) { + var bookmark = bookmark + + // If URL changed update URL first as updating the Bookmark altogether will throw an error as the bookmark can't be fetched by URL. + if bookmark.url != url.absoluteString { + bookmark = bookmarkManager.updateUrl(of: bookmark, to: url) ?? bookmark + } + + if bookmark.title != name || bookmark.isFavorite != isBookmarkFavorite { + bookmark.title = name + bookmark.isFavorite = isBookmarkFavorite + bookmarkManager.update(bookmark: bookmark) + } + if bookmark.parentFolderUUID != selectedFolder?.id { + let parentFoler: ParentFolderType = selectedFolder.flatMap { .parent(uuid: $0.id) } ?? .root + bookmarkManager.move(objectUUIDs: [bookmark.id], toIndex: nil, withinParentFolder: parentFoler, completion: { _ in }) + } + } + + func addBookmark(withURL url: URL, name: String, isFavorite: Bool, to parent: BookmarkFolder?) { + bookmarkManager.makeBookmark(for: url, title: name, isFavorite: isFavorite, index: nil, parent: parent) + } +} + +private extension AddEditBookmarkDialogViewModel.Mode { + + var title: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Title.addBookmark + case .edit: + return UserText.Bookmarks.Dialog.Title.editBookmark + } + } + + var cancelActionTitle: String { + switch self { + case .add, .edit: + return UserText.cancel + } + } + + var defaultActionTitle: String { + switch self { + case .add: + return UserText.Bookmarks.Dialog.Action.addBookmark + case .edit: + return UserText.save + } + } + + var bookmark: Bookmark? { + switch self { + case .add: + return nil + case let .edit(bookmark): + return bookmark + } + } + +} diff --git a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift index a1da3c5262..62c1e0356c 100644 --- a/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift +++ b/DuckDuckGo/Bookmarks/ViewModel/AddEditBookmarkFolderDialogViewModel.swift @@ -20,18 +20,13 @@ import Foundation import Combine @MainActor -protocol BookmarkFolderDialogViewModel: ObservableObject { - var title: String { get } - var folderName: String { get } - var folders: [FolderViewModel] { get } - var selectedFolder: BookmarkFolder? { get } - - func cancel() - func addOrSave() +protocol BookmarkFolderDialogEditing: BookmarksDialogViewModel { + var addFolderPublisher: AnyPublisher { get } + var folderName: String { get set } } @MainActor -final class AddEditBookmarkFolderDialogViewModel: ObservableObject { +final class AddEditBookmarkFolderDialogViewModel: BookmarkFolderDialogEditing { /// The type of operation to perform on a folder enum Mode { @@ -62,14 +57,19 @@ final class AddEditBookmarkFolderDialogViewModel: ObservableObject { mode.defaultActionTitle } - let isCancelActionDisabled = false + let isOtherActionDisabled = false - var isDefaultActionButtonDisabled: Bool { + var isDefaultActionDisabled: Bool { folderName.trimmingWhitespace().isEmpty } + var addFolderPublisher: AnyPublisher { + addFolderSubject.eraseToAnyPublisher() + } + private let mode: Mode private let bookmarkManager: BookmarkManager + private let addFolderSubject: PassthroughSubject = .init() init(mode: Mode, bookmarkManager: BookmarkManager = LocalBookmarkManager.shared) { self.mode = mode @@ -124,7 +124,9 @@ private extension AddEditBookmarkFolderDialogViewModel { } func add(folderWithName name: String, to parent: BookmarkFolder?) { - bookmarkManager.makeFolder(for: name, parent: parent, completion: { _ in }) + bookmarkManager.makeFolder(for: name, parent: parent) { [weak self] bookmarkFolder in + self?.addFolderSubject.send(bookmarkFolder) + } } } diff --git a/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift new file mode 100644 index 0000000000..08ef2cc47d --- /dev/null +++ b/DuckDuckGo/Bookmarks/ViewModel/BookmarksDialogViewModel.swift @@ -0,0 +1,35 @@ +// +// BookmarksDialogViewModel.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 Foundation + +@MainActor +protocol BookmarksDialogViewModel: ObservableObject { + var title: String { get } + + var folders: [FolderViewModel] { get } + var selectedFolder: BookmarkFolder? { get set } + + var cancelActionTitle: String { get } + var isOtherActionDisabled: Bool { get } + var defaultActionTitle: String { get } + var isDefaultActionDisabled: Bool { get } + + func cancel(dismiss: () -> Void) + func addOrSave(dismiss: () -> Void) +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift new file mode 100644 index 0000000000..53072513c4 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogCoordinatorViewModelTests.swift @@ -0,0 +1,170 @@ +// +// AddEditBookmarkDialogCoordinatorViewModelTests.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 +import Combine +@testable import DuckDuckGo_Privacy_Browser + +@MainActor +final class AddEditBookmarkDialogCoordinatorViewModelTests: XCTestCase { + private var sut: AddEditBookmarkDialogCoordinatorViewModel! + private var bookmarkViewModelMock: AddEditBookmarkDialogViewModelMock! + private var bookmarkFolderViewModelMock: AddEditBookmarkFolderDialogViewModelMock! + private var cancellables: Set! + + override func setUpWithError() throws { + try super.setUpWithError() + + cancellables = [] + bookmarkViewModelMock = .init() + bookmarkFolderViewModelMock = .init() + sut = .init(bookmarkModel: bookmarkViewModelMock, folderModel: bookmarkFolderViewModelMock) + } + + override func tearDownWithError() throws { + cancellables = nil + bookmarkViewModelMock = nil + bookmarkFolderViewModelMock = nil + sut = nil + try super.tearDownWithError() + } + + func testShouldReturnViewStateBookmarkWhenInit() { + XCTAssertEqual(sut.viewState, .bookmark) + } + + func testShouldReturnViewStateBookmarkWhenDismissActionIsCalled() { + // GIVEN + sut.addFolderAction() + XCTAssertEqual(sut.viewState, .folder) + + // WHEN + sut.dismissAction() + + // THEN + XCTAssertEqual(sut.viewState, .bookmark) + + } + + func testShouldSetSelectedFolderOnFolderViewModelAndReturnFolderViewStateWhenAddFolderActionIsCalled() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: "Folder") + bookmarkViewModelMock.selectedFolder = folder + XCTAssertNil(bookmarkFolderViewModelMock.selectedFolder) + + // WHEN + sut.addFolderAction() + + // THEN + XCTAssertEqual(bookmarkFolderViewModelMock.selectedFolder, folder) + } + + func testShouldReceiveEventsWhenBookmarkModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.bookmarkModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldReceiveEventsWhenBookmarkFolderModelChanges() { + // GIVEN + let expectation = self.expectation(description: #function) + var didCallChangeValue = false + sut.objectWillChange.sink { _ in + didCallChangeValue = true + expectation.fulfill() + } + .store(in: &cancellables) + + // WHEN + sut.folderModel.objectWillChange.send() + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertTrue(didCallChangeValue) + } + + func testShouldSetSelectedFolderOnBookmarkViewModelWhenAddFolderPublisherSendsEvent() { + // GIVEN + let expectation = self.expectation(description: #function) + bookmarkViewModelMock.selectedFolderExpectation = expectation + let folder = BookmarkFolder(id: "ABCDE", title: #function) + XCTAssertNil(bookmarkViewModelMock.selectedFolder) + + // WHEN + sut.folderModel.subject.send(folder) + + // THEN + waitForExpectations(timeout: 1.0) + XCTAssertEqual(bookmarkViewModelMock.selectedFolder, folder) + } + +} + +final class AddEditBookmarkDialogViewModelMock: BookmarkDialogEditing { + var bookmarkName: String = "" + var bookmarkURLPath: String = "" + var isBookmarkFavorite: Bool = false + var isURLFieldHidden: Bool = false + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? { + didSet { + selectedFolderExpectation?.fulfill() + } + } + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} + + var selectedFolderExpectation: XCTestExpectation? +} + +final class AddEditBookmarkFolderDialogViewModelMock: BookmarkFolderDialogEditing { + let subject = PassthroughSubject() + + var addFolderPublisher: AnyPublisher { + subject.eraseToAnyPublisher() + } + var folderName: String = "" + var title: String = "" + var folders: [DuckDuckGo_Privacy_Browser.FolderViewModel] = [] + var selectedFolder: DuckDuckGo_Privacy_Browser.BookmarkFolder? + var cancelActionTitle: String = "" + var isOtherActionDisabled: Bool = false + var defaultActionTitle: String = "" + var isDefaultActionDisabled: Bool = false + + func cancel(dismiss: () -> Void) {} + func addOrSave(dismiss: () -> Void) {} +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift new file mode 100644 index 0000000000..9268b39468 --- /dev/null +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkDialogViewModelTests.swift @@ -0,0 +1,591 @@ +// +// AddEditBookmarkDialogViewModelTests.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 AddEditBookmarkDialogViewModelTests: XCTestCase { + private var bookmarkManager: LocalBookmarkManager! + private var bookmarkStoreMock: BookmarkStoreMock! + + override func setUpWithError() throws { + try super.setUpWithError() + bookmarkStoreMock = BookmarkStoreMock() + bookmarkStoreMock.bookmarks = [BookmarkFolder.mock] + bookmarkManager = .init(bookmarkStore: bookmarkStoreMock, faviconManagement: FaviconManagerMock()) + bookmarkManager.loadBookmarks() + } + + override func tearDownWithError() throws { + bookmarkStoreMock = nil + bookmarkManager = nil + try super.tearDownWithError() + } + + // MARK: - Copy + + func testReturnAddBookmarkTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.addBookmark) + } + + func testReturnEditBookmarkTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.title + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Title.editBookmark) + } + + func testReturnCancelActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnCancelActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.cancelActionTitle + + // THEN + XCTAssertEqual(title, UserText.cancel) + } + + func testReturnAddBookmarkActionTitleWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.Bookmarks.Dialog.Action.addBookmark) + } + + func testReturnSaveActionTitleWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let title = sut.defaultActionTitle + + // THEN + XCTAssertEqual(title, UserText.save) + } + + // MARK: State + + func testShouldSetBookmarkNameToEmptyWhenInitAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertTrue(result.isEmpty) + } + + func testShouldSetBookmarkNameToValueWhenInitAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.bookmarkName + + // THEN + XCTAssertEqual(result, #function) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetFoldersFromBookmarkListWhenInitAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.folders + + // THEN + XCTAssertEqual(result.count, 1) + XCTAssertEqual(result.first?.entity, folder) + } + + func testShouldSetSelectedFolderToNilWhenBookmarkParentFolderIsNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "2") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + func testShouldSetSelectedFolderToNilWhenParentFolderIsNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "2") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertNil(result) + } + + func testShouldSetSelectedFolderToValueWhenParentFolderIsNotNilAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "1", title: #function) + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "1") + bookmarkStoreMock.bookmarks = [folder] + bookmarkManager.loadBookmarks() + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.selectedFolder + + // THEN + XCTAssertEqual(result, folder) + } + + // MARK: - Actions + + func testReturnIsCancelActionDisabledFalseWhenModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsCancelActionDisabledFalseWhenModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + + // WHEN + let result = sut.isOtherActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkNameIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkNameIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + // ------- + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarURLIsEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledTrueWhenBookmarkURLIsEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = "" + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertTrue(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsAdd() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testReturnIsDefaultActionButtonDisabledFalseWhenBookmarkURLIsNotEmptyAndModeIsEdit() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: .mock), bookmarkManager: bookmarkManager) + sut.bookmarkName = " DuckDuckGo " + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + + // WHEN + let result = sut.isDefaultActionDisabled + + // THEN + XCTAssertFalse(result) + } + + func testShouldCallDismissWhenCancelIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + var didCallDismiss = false + + // WHEN + sut.cancel { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldCallDismissWhenAddOrSaveIsCalled() { + // GIVEN + let sut = AddEditBookmarkDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) + sut.bookmarkName = "DuckDuckGo" + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + var didCallDismiss = false + + // WHEN + sut.addOrSave { + didCallDismiss = true + } + + // THEN + XCTAssertTrue(didCallDismiss) + } + + func testShouldAskBookmarkStoreToSaveBookmarkWhenModeIsAdd() { + // GIVEN + let folder = BookmarkFolder(id: #file, title: #function) + let sut = AddEditBookmarkDialogViewModel(mode: .add(parentFolder: folder), bookmarkManager: bookmarkManager) + sut.bookmarkName = #function + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + XCTAssertNil(bookmarkStoreMock.capturedParentFolder) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.title, #function) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark?.url, URL.duckDuckGo.absoluteString) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolder, folder) + } + + func testShouldAskBookmarkStoreToUpdateURLWhenURLIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.exti, title: "DuckDuckGo", isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = expectedBookmark.url + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateURLWhenURLIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + sut.bookmarkURLPath = URL.duckDuckGo.absoluteString + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenNameIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = expectedBookmark.title + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenNameIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.bookmarkName = #function + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let expectedBookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: "DuckDuckGo", isFavorite: true) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = expectedBookmark.isFavorite + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertTrue(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertEqual(bookmarkStoreMock.capturedBookmark, expectedBookmark) + } + + func testShouldNotAskBookmarkStoreToUpdateBookmarkWhenIsFavoriteIsNotUpdatedAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.isBookmarkFavorite = false + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertNil(bookmarkStoreMock.capturedBookmark) + } + + func testShouldAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder") + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [folder, bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertTrue(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertEqual(bookmarkStoreMock.capturedObjectUUIDs, [bookmark.id]) + XCTAssertEqual(bookmarkStoreMock.capturedParentFolderType, .parent(uuid: folder.id)) + } + + func testShouldNotAskBookmarkStoreToMoveBookmarkWhenSelectedFolderIsNotDifferentFromOriginalFolderAndModeIsEdit() { + // GIVEN + let bookmark = Bookmark(id: "1", url: URL.duckDuckGo.absoluteString, title: #function, isFavorite: false, parentFolderUUID: "ABCDE") + let folder = BookmarkFolder(id: "ABCDE", title: "Test Folder", children: [bookmark]) + let sut = AddEditBookmarkDialogViewModel(mode: .edit(bookmark: bookmark), bookmarkManager: bookmarkManager) + bookmarkStoreMock.bookmarks = [bookmark] + bookmarkManager.loadBookmarks() + sut.selectedFolder = folder + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + + // WHEN + sut.addOrSave {} + + // THEN + XCTAssertFalse(bookmarkStoreMock.saveBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.updateBookmarkCalled) + XCTAssertFalse(bookmarkStoreMock.moveObjectUUIDCalled) + XCTAssertNil(bookmarkStoreMock.capturedObjectUUIDs) + XCTAssertNil(bookmarkStoreMock.capturedParentFolderType) + } +} diff --git a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift index 135fc69cbf..a48f18d2ab 100644 --- a/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift +++ b/UnitTests/Bookmarks/ViewModels/AddEditBookmarkFolderDialogViewModelTests.swift @@ -220,7 +220,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) // WHEN - let result = sut.isCancelActionDisabled + let result = sut.isOtherActionDisabled // THEN XCTAssertFalse(result) @@ -231,7 +231,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: .mock, parentFolder: nil), bookmarkManager: bookmarkManager) // WHEN - let result = sut.isCancelActionDisabled + let result = sut.isOtherActionDisabled // THEN XCTAssertFalse(result) @@ -243,7 +243,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = "" // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertTrue(result) @@ -255,7 +255,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = "" // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertTrue(result) @@ -267,7 +267,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = " Test " // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertFalse(result) @@ -279,7 +279,7 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { sut.folderName = " Test " // WHEN - let result = sut.isDefaultActionButtonDisabled + let result = sut.isDefaultActionDisabled // THEN XCTAssertFalse(result) @@ -303,9 +303,10 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { // GIVEN let sut = AddEditBookmarkFolderDialogViewModel(mode: .add(), bookmarkManager: bookmarkManager) var didCallDismiss = false + sut.folderName = "DuckDuckGo" // WHEN - sut.cancel { + sut.addOrSave { didCallDismiss = true } @@ -385,7 +386,6 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { func testShouldAskBookmarkStoreToMoveFolderToRootFolderWhenSelectedFolderIsDifferentFromOriginalFolder() { // GIVEN - let location = BookmarkFolder(id: #file, title: #function) let folder = BookmarkFolder.mock let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: .mock), bookmarkManager: bookmarkManager) sut.selectedFolder = nil @@ -405,7 +405,6 @@ final class AddEditBookmarkFolderDialogViewModelTests: XCTestCase { func testShouldNotAskBookmarkStoreToMoveFolderWhenSelectedFolderIsNotDifferentFromOriginalFolder() { // GIVEN - let location = BookmarkFolder(id: #file, title: #function) let folder = BookmarkFolder.mock let sut = AddEditBookmarkFolderDialogViewModel(mode: .edit(folder: folder, parentFolder: nil), bookmarkManager: bookmarkManager) sut.selectedFolder = nil