diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 1daa63df7b..6426014454 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -2863,6 +2863,12 @@ BB731F312CDBA6360023D2E4 /* FireWindowTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */; }; BB7B5F982C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; BB7B5F992C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */; }; + BB9BA2202D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */; }; + BB9BA2212D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */; }; + BB9BA2262D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */; }; + BB9BA2272D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */; }; + BB9BA2292D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */; }; + BB9BA22A2D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */; }; BB9BDD492D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */; }; BB9BDD4A2D09BAA80069E9EF /* TabBarRemoteMessageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */; }; BBB881882C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */; }; @@ -4831,6 +4837,9 @@ BB5F46A22C8751F6005F72DF /* BookmarkSortTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSortTests.swift; sourceTree = ""; }; BB731F302CDBA6320023D2E4 /* FireWindowTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FireWindowTests.swift; sourceTree = ""; }; BB7B5F972C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksSearchAndSortMetrics.swift; sourceTree = ""; }; + BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageViewModelTests.swift; sourceTree = ""; }; + BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessage.swift; sourceTree = ""; }; + BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarActiveRemoteMessage.swift; sourceTree = ""; }; BB9BDD482D09BA9D0069E9EF /* TabBarRemoteMessageViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TabBarRemoteMessageViewModel.swift; sourceTree = ""; }; BBB881872C4029BA001247C6 /* BookmarkListTreeControllerSearchDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkListTreeControllerSearchDataSource.swift; sourceTree = ""; }; BBBB653F2C77BB9400E69AC6 /* BookmarkSearchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarkSearchTests.swift; sourceTree = ""; }; @@ -8525,6 +8534,8 @@ AA9FF95724A1ECE20039E328 /* Model */ = { isa = PBXGroup; children = ( + BB9BA2282D10C0A3009229F3 /* TabBarActiveRemoteMessage.swift */, + BB9BA2252D10C089009229F3 /* TabBarRemoteMessage.swift */, AA9FF95C24A1FA1C0039E328 /* TabCollection.swift */, ); path = Model; @@ -8737,6 +8748,7 @@ AAC9C01A24CB592E00AD1325 /* ViewModel */ = { isa = PBXGroup; children = ( + BB9BA21F2D10BC6B009229F3 /* TabBarRemoteMessageViewModelTests.swift */, AAC9C01D24CB6BEB00AD1325 /* TabCollectionViewModelTests.swift */, 37D23788288009CF00BCE03B /* TabCollectionViewModelTests+PinnedTabs.swift */, 37479F142891BC8300302FE2 /* TabCollectionViewModelTests+WithoutPinnedTabsManager.swift */, @@ -11267,6 +11279,7 @@ BB7B5F992C4ED73800BA4AF8 /* BookmarksSearchAndSortMetrics.swift in Sources */, B60D644A2AAF1B7C00B26F50 /* AddressBarTextSelectionNavigation.swift in Sources */, 1D01A3D52B88CF7700FE8150 /* AccessibilityPreferences.swift in Sources */, + BB9BA2262D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */, 37219B342CBFBBE800C9D7A8 /* NewTabPageSearchBoxExperiment.swift in Sources */, 3706FA98293F65D500E42796 /* SecureVaultSorting.swift in Sources */, 1DEDB3652C19934C006B6D1B /* MoreOptionsMenuButton.swift in Sources */, @@ -12091,6 +12104,7 @@ 3706FC6F293F65D500E42796 /* FirePopoverViewModel.swift in Sources */, 3706FC71293F65D500E42796 /* NSColorExtension.swift in Sources */, 1DB9618229F67F6100CF5568 /* FaviconNullStore.swift in Sources */, + BB9BA22A2D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */, 3706FC73293F65D500E42796 /* AddressBarButtonsViewController.swift in Sources */, 9FDA6C222B79A59D00E099A9 /* BookmarkFavoriteView.swift in Sources */, C1372EF52BBC5BAD003F8793 /* SecureTextField.swift in Sources */, @@ -12292,6 +12306,7 @@ 3706FE1C293F661700E42796 /* ConnectBitwardenViewModelTests.swift in Sources */, 4B9DB0552A983B55000927DB /* MockWaitlistStorage.swift in Sources */, 1D9FDEC72B9B64DB0040B78C /* PrivacyProtectionStatusTests.swift in Sources */, + BB9BA2202D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */, C13909F52B85FD79001626ED /* AutofillDeleteAllPasswordsExecutorTests.swift in Sources */, 37D046A52C7DAA8900AEAA50 /* ImageProcessorMock.swift in Sources */, 857E44642A9F70F200ED77A7 /* CampaignVariantTests.swift in Sources */, @@ -12940,6 +12955,7 @@ B693955326F04BEC0015B914 /* WindowDraggingView.swift in Sources */, 4B1E6EED27AB5E5100F51793 /* SecureVaultSorting.swift in Sources */, 37CD54CE27F2FDD100F1F7B9 /* PreferencesSidebarModel.swift in Sources */, + BB9BA2272D10C08F009229F3 /* TabBarRemoteMessage.swift in Sources */, 1D43EB32292788C70065E5D6 /* BWEncryptionOutput.m in Sources */, 3707EC4A2C47E36A00B67CBE /* CloseButton.swift in Sources */, B6106BAD26A7BF390013B453 /* PermissionState.swift in Sources */, @@ -13546,6 +13562,7 @@ AA2CB1352587C29500AA6FBE /* TabBarFooter.swift in Sources */, EEC111E6294D06290086524F /* JSAlertViewModel.swift in Sources */, 4BE5336C286912D40019DBFD /* BookmarksBarCollectionViewItem.swift in Sources */, + BB9BA2292D10C0A5009229F3 /* TabBarActiveRemoteMessage.swift in Sources */, B6C0B23926E742610031CB7F /* FileDownloadError.swift in Sources */, 85589EA027BFE60E0038AD11 /* MoreOrLessView.swift in Sources */, B6CC26682BAD959500F53F8D /* DownloadProgress.swift in Sources */, @@ -14030,6 +14047,7 @@ B63ED0DC26AE7B1E00A9DAD1 /* WebViewMock.swift in Sources */, 56A053FF2C1AEFA1007D8FAB /* OnboardingManagerTests.swift in Sources */, 4B4F72EC266B2ED300814C60 /* CollectionExtension.swift in Sources */, + BB9BA2212D10BC72009229F3 /* TabBarRemoteMessageViewModelTests.swift in Sources */, C172E7332C93759C00521D9A /* SyncPromoManagerTests.swift in Sources */, AAE39D1B24F44885008EF28B /* TabCollectionViewModelDelegateMock.swift in Sources */, 9F0A2CF82B96A58600C5B8C0 /* BaseBookmarkEntityTests.swift in Sources */, diff --git a/DuckDuckGo/TabBar/Model/TabBarActiveRemoteMessage.swift b/DuckDuckGo/TabBar/Model/TabBarActiveRemoteMessage.swift new file mode 100644 index 0000000000..9ee658c0d0 --- /dev/null +++ b/DuckDuckGo/TabBar/Model/TabBarActiveRemoteMessage.swift @@ -0,0 +1,52 @@ +// +// TabBarActiveRemoteMessage.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 Combine +import RemoteMessaging + +protocol TabBarRemoteMessageProviding { + var remoteMessagePublisher: AnyPublisher { get } + + func markRemoteMessageAsShown() async + func onSurveyOpened() async + func onMessageDismissed() async +} + +final class TabBarActiveRemoteMessage: TabBarRemoteMessageProviding { + private let activeRemoteMessageModel: ActiveRemoteMessageModel + + var remoteMessagePublisher: AnyPublisher { + activeRemoteMessageModel.$remoteMessage.eraseToAnyPublisher() + } + + init(activeRemoteMessageModel: ActiveRemoteMessageModel) { + self.activeRemoteMessageModel = activeRemoteMessageModel + } + + func markRemoteMessageAsShown() async { + await activeRemoteMessageModel.markRemoteMessageAsShown() + } + + func onSurveyOpened() async { + await activeRemoteMessageModel.dismissRemoteMessage(with: .primaryAction) + } + + func onMessageDismissed() async { + await activeRemoteMessageModel.dismissRemoteMessage(with: .close) + } +} diff --git a/DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift b/DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift new file mode 100644 index 0000000000..02aa30c1f3 --- /dev/null +++ b/DuckDuckGo/TabBar/Model/TabBarRemoteMessage.swift @@ -0,0 +1,26 @@ +// +// TabBarRemoteMessage.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. +// + +struct TabBarRemoteMessage { + static let tabBarPermanentSurveyRemoteMessageId = "macos_permanent_survey_tab_bar" + + let buttonTitle: String + let popupTitle: String + let popupSubtitle: String + let surveyURL: URL +} diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift index 3bf4c5b6b4..34d638a76b 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageView.swift @@ -26,6 +26,7 @@ struct TabBarRemoteMessageView: View { let onTap: (URL) -> Void let onHover: () -> Void let onHoverEnd: () -> Void + let onAppear: () -> Void var body: some View { HStack { @@ -42,6 +43,7 @@ struct TabBarRemoteMessageView: View { onHoverEnd() }) ) + .onAppear(perform: { onAppear() }) .frame(width: 147) } } diff --git a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift index 9547215b1c..0a8f3c3eff 100644 --- a/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift +++ b/DuckDuckGo/TabBar/TabBarRemoteMessaging/TabBarRemoteMessageViewModel.swift @@ -19,27 +19,20 @@ import Combine import RemoteMessaging -struct TabBarRemoteMessage { - static let tabBarPermanentSurveyRemoteMessageId = "macos_permanent_survey_tab_bar" - - let buttonTitle: String - let popupTitle: String - let popupSubtitle: String - let surveyURL: URL -} - final class TabBarRemoteMessageViewModel: ObservableObject { - private let activeRemoteMessageModel: ActiveRemoteMessageModel + private let tabBarRemoteActiveMessage: TabBarRemoteMessageProviding private var cancellable: AnyCancellable? @Published var remoteMessage: TabBarRemoteMessage? - init(activeRemoteMessageModel: ActiveRemoteMessageModel) { - self.activeRemoteMessageModel = activeRemoteMessageModel + init(activeRemoteMessageModel: TabBarRemoteMessageProviding, isFireWindow: Bool) { + self.tabBarRemoteActiveMessage = activeRemoteMessageModel - cancellable = activeRemoteMessageModel.$remoteMessage + cancellable = tabBarRemoteActiveMessage.remoteMessagePublisher .sink(receiveValue: { model in + guard !isFireWindow else { return } + guard let model = model else { self.remoteMessage = nil return @@ -51,18 +44,16 @@ final class TabBarRemoteMessageViewModel: ObservableObject { }) } - func onDismiss() { - Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .close) } + func onSurveyOpened() { + Task { await tabBarRemoteActiveMessage.onSurveyOpened() } } - /// When the user hovers the Tab Bar Remote Message and we show the popup, there is where when we mark - /// that the user really saw the message. - func onUserHovered() { - Task { await activeRemoteMessageModel.markRemoteMessageAsShown() } + func onMessageDismissed() { + Task { await tabBarRemoteActiveMessage.onMessageDismissed() } } - func onOpenSurvey() { - Task { await activeRemoteMessageModel.dismissRemoteMessage(with: .primaryAction) } + func markTabBarRemoteMessageAsShown() { + Task { await tabBarRemoteActiveMessage.markRemoteMessageAsShown() } } } diff --git a/DuckDuckGo/TabBar/View/TabBarViewController.swift b/DuckDuckGo/TabBar/View/TabBarViewController.swift index 0bdd871fb7..37aafe6888 100644 --- a/DuckDuckGo/TabBar/View/TabBarViewController.swift +++ b/DuckDuckGo/TabBar/View/TabBarViewController.swift @@ -103,7 +103,9 @@ final class TabBarViewController: NSViewController { init?(coder: NSCoder, tabCollectionViewModel: TabCollectionViewModel, activeRemoteMessageModel: ActiveRemoteMessageModel) { self.tabCollectionViewModel = tabCollectionViewModel - self.tabBarRemoteMessageViewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: activeRemoteMessageModel) + let tabBarActiveRemoteMessageModel = TabBarActiveRemoteMessage(activeRemoteMessageModel: activeRemoteMessageModel) + self.tabBarRemoteMessageViewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: tabBarActiveRemoteMessageModel, + isFireWindow: tabCollectionViewModel.isBurner) if !tabCollectionViewModel.isBurner, let pinnedTabCollection = tabCollectionViewModel.pinnedTabsManager?.tabCollection { let pinnedTabsViewModel = PinnedTabsViewModel(collection: pinnedTabCollection) let pinnedTabsView = PinnedTabsView(model: pinnedTabsViewModel) @@ -226,20 +228,22 @@ final class TabBarViewController: NSViewController { let feedbackButtonView = TabBarRemoteMessageView( model: tabBarRemotMessage, onClose: { - self.tabBarRemoteMessageViewModel.onDismiss() + self.tabBarRemoteMessageViewModel.onMessageDismissed() self.removeFeedbackButton() }, onTap: { surveyURL in WindowControllersManager.shared.showTab(with: .contentFromURL(surveyURL, source: .appOpenUrl)) - self.tabBarRemoteMessageViewModel.onOpenSurvey() + self.tabBarRemoteMessageViewModel.onSurveyOpened() self.removeFeedbackButton() }, onHover: { self.startTabBarRemotMessageTimer(message: tabBarRemotMessage) - self.tabBarRemoteMessageViewModel.onUserHovered() }, onHoverEnd: { self.dismissTabBarRemoteMessagePopover() + }, + onAppear: { + self.tabBarRemoteMessageViewModel.markTabBarRemoteMessageAsShown() } ) feedbackBarButtonHostingController = NSHostingController(rootView: feedbackButtonView) @@ -258,7 +262,7 @@ final class TabBarViewController: NSViewController { private func startTabBarRemotMessageTimer(message: TabBarRemoteMessage) { tabBarRemoteMessagePopoverHoverTimer?.invalidate() - tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { _ in + tabBarRemoteMessagePopoverHoverTimer = Timer.scheduledTimer(withTimeInterval: 1.5, repeats: false) { _ in self.showTabBarRemotePopup(message) } } @@ -269,31 +273,33 @@ final class TabBarViewController: NSViewController { } private func showTabBarRemotePopup(_ message: TabBarRemoteMessage) { - if let popover = tabBarRemoteMessagePopover { - guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { - return - } + guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { + return + } + if let popover = tabBarRemoteMessagePopover { popover.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) } else { tabBarRemoteMessagePopover = NSPopover() - tabBarRemoteMessagePopover?.animates = true - tabBarRemoteMessagePopover?.behavior = .semitransient - tabBarRemoteMessagePopover?.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, - height: TabBarRemoteMessagePopoverContent.Constants.height) - - let controller = NSViewController() - controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) - tabBarRemoteMessagePopover?.contentViewController = controller - - guard let tabBarButtonRemoteMessageView = feedbackBarButtonHostingController?.view else { - return - } + configurePopover(with: message) tabBarRemoteMessagePopover?.show(positionedBelow: tabBarButtonRemoteMessageView.bounds, in: tabBarButtonRemoteMessageView) } } + private func configurePopover(with message: TabBarRemoteMessage) { + guard let popover = tabBarRemoteMessagePopover else { return } + + popover.animates = true + popover.behavior = .semitransient + popover.contentSize = NSSize(width: TabBarRemoteMessagePopoverContent.Constants.width, + height: TabBarRemoteMessagePopoverContent.Constants.height) + + let controller = NSViewController() + controller.view = NSHostingView(rootView: TabBarRemoteMessagePopoverContent(model: message)) + popover.contentViewController = controller + } + private func removeFeedbackButton() { guard let hostingController = feedbackBarButtonHostingController else { return } diff --git a/UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift b/UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift new file mode 100644 index 0000000000..a1c691a58e --- /dev/null +++ b/UnitTests/TabBar/ViewModel/TabBarRemoteMessageViewModelTests.swift @@ -0,0 +1,159 @@ +// +// TabBarRemoteMessageViewModelTests.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 +import RemoteMessaging +@testable import DuckDuckGo_Privacy_Browser + +class TabBarRemoteMessageViewModelTests: XCTestCase { + private var cancellables: Set = [] + + func testWhenModelIsNotForTabBar_thenIsNotSetAsRemoteMessage() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: false) + let expectation = XCTestExpectation(description: "Publisher should emit a nil value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage == nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createOtherRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenModelIsForTabBarButIsMalformed_thenIsNotSetAsRemoteMessage() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: false) + let expectation = XCTestExpectation(description: "Publisher should emit a nil value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage == nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createMalformedTabBarRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenWindowIsFireWindow_thenIsNotSetAsRemoteMessage() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: true) + let expectation = XCTestExpectation(description: "Publisher should emit a nil value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage == nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createTabBarRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + func testWhenTabBarRemoteMessageIsCorrect_thenIsSet() { + let mock = MockTabBarRemoteMessageProvider() + let viewModel = TabBarRemoteMessageViewModel(activeRemoteMessageModel: mock, isFireWindow: false) + let expectation = XCTestExpectation(description: "Publisher should not emit a value") + + viewModel.$remoteMessage + .sink { remoteMesssage in + if remoteMesssage != nil { + expectation.fulfill() + } + }.store(in: &cancellables) + + mock.emitRemoteMessage(createTabBarRemoteMessage()) + + wait(for: [expectation], timeout: 1.0) + } + + // MARK: - Utilities + + private func createTabBarRemoteMessage() -> RemoteMessageModel { + let tabBarRemoteMessageContent: RemoteMessageModelType = .bigSingleAction(titleText: "Help Us Improve", + descriptionText: "We really want to know which features would make our browser better.", + placeholder: .announce, + primaryActionText: "Tell Us What You Think", + primaryAction: .survey(value: "www.survey.com")) + return RemoteMessageModel(id: TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId, + content: tabBarRemoteMessageContent, + matchingRules: [Int](), + exclusionRules: [Int](), + isMetricsEnabled: true) + } + + private func createMalformedTabBarRemoteMessage() -> RemoteMessageModel { + let tabBarRemoteMessageContent: RemoteMessageModelType = .bigSingleAction(titleText: "Help Us Improve", + descriptionText: "We really want to know which features would make our browser better.", + placeholder: .announce, + primaryActionText: "Tell Us What You Think", + primaryAction: .appStore) + return RemoteMessageModel(id: TabBarRemoteMessage.tabBarPermanentSurveyRemoteMessageId, + content: tabBarRemoteMessageContent, + matchingRules: [Int](), + exclusionRules: [Int](), + isMetricsEnabled: true) + } + + private func createOtherRemoteMessage() -> RemoteMessageModel { + let tabBarRemoteMessageContent: RemoteMessageModelType = .bigSingleAction(titleText: "Some title!", + descriptionText: "Some description", + placeholder: .announce, + primaryActionText: "Primary!", + primaryAction: .survey(value: "www.survey.com")) + return RemoteMessageModel(id: "other_id", + content: tabBarRemoteMessageContent, + matchingRules: [Int](), + exclusionRules: [Int](), + isMetricsEnabled: true) + } +} + +class MockTabBarRemoteMessageProvider: TabBarRemoteMessageProviding { + private let remoteMessageSubject = PassthroughSubject() + + var remoteMessagePublisher: AnyPublisher { + return remoteMessageSubject.eraseToAnyPublisher() + } + + func emitRemoteMessage(_ message: RemoteMessageModel?) { + remoteMessageSubject.send(message) + } + + func markRemoteMessageAsShown() async { + // No-op + } + + func onSurveyOpened() async { + // No-op + } + + func onMessageDismissed() async { + // No-op + } +}