diff --git a/Tests/MessagingInApp/Gist/Managers/MessageQueueManagerIntegrationTest.swift b/Tests/MessagingInApp/Gist/Managers/MessageQueueManagerIntegrationTest.swift index 38881a735..6cc608c8a 100644 --- a/Tests/MessagingInApp/Gist/Managers/MessageQueueManagerIntegrationTest.swift +++ b/Tests/MessagingInApp/Gist/Managers/MessageQueueManagerIntegrationTest.swift @@ -1,73 +1,73 @@ -@testable import CioInternalCommon -@testable import CioMessagingInApp -import SharedTests -import XCTest - -class MessageQueueManagerIntegrationTests: IntegrationTest { - var manager: MessageQueueManager! - - var sampleFetchResponseBody: String { - readSampleDataFile(subdirectory: "InAppUserQueue", fileName: "fetch_response.json") - } - - override func setUp() { - super.setUp() - - initializeManager() - - UserManager().setUserToken(userToken: .random) // Set a user token so manager can perform user queue fetches. - } - - // MARK: fetch user messages from backend services - - func test_fetch_givenHTTPResponse200_expectSetLocalMessageStoreFromFetchResponse() { - XCTAssertTrue(manager.localMessageStore.isEmpty) - - setupHttpResponse(code: 200, body: sampleFetchResponseBody.data) - manager.fetchUserMessages() - - XCTAssertEqual(manager.localMessageStore.count, 2) - } - - func test_fetch_givenMessageCacheSaved_given304AfterSdkInitialized_expectPopulateLocalMessageStoreFromCache() { - XCTAssertTrue(manager.localMessageStore.isEmpty) - - setupHttpResponse(code: 200, body: sampleFetchResponseBody.data) - manager.fetchUserMessages() - XCTAssertEqual(manager.localMessageStore.count, 2) - - let localMessageStoreBefore304: [Message] = manager.localMessageStore.values.compactMap { $0 } - - initializeManager() - XCTAssertTrue(manager.localMessageStore.isEmpty) - - setupHttpResponse(code: 304, body: "".data) - manager.fetchUserMessages() - - XCTAssertEqual(manager.localMessageStore.count, 2) - let localMessageStoreAfter304 = manager.localMessageStore.values - - localMessageStoreBefore304.forEach { message in - XCTAssertTrue(localMessageStoreAfter304.contains(message)) - } - } - - // The SDK could receive a 304 and the SDK does not have a previous fetch response cached. Example use cases when this could happen: - // 1. The user logs out of the SDK and logs in again with same or different profile. - // 2. Reinstalls the app and first fetch response is a 304 - func test_fetch_givenNoPreviousCacheSaved_given304AfterSdkInitialized_expectPopulateLocalMessageStoreFromCache() { - XCTAssertTrue(manager.localMessageStore.isEmpty) - - setupHttpResponse(code: 304, body: "".data) - manager.fetchUserMessages() - - XCTAssertTrue(manager.localMessageStore.isEmpty) - } -} - -extension MessageQueueManagerIntegrationTests { - // Convenient function for test functions that need to test when a new instance of manager is created (clearing in-memory stores). - func initializeManager() { - manager = MessageQueueManager() - } -} +// @testable import CioInternalCommon +// @testable import CioMessagingInApp +// import SharedTests +// import XCTest +// +// class MessageQueueManagerIntegrationTests: IntegrationTest { +// var manager: MessageQueueManager! +// +// var sampleFetchResponseBody: String { +// readSampleDataFile(subdirectory: "InAppUserQueue", fileName: "fetch_response.json") +// } +// +// override func setUp() { +// super.setUp() +// +// initializeManager() +// +// UserManager().setUserToken(userToken: .random) // Set a user token so manager can perform user queue fetches. +// } +// +// // MARK: fetch user messages from backend services +// +// func test_fetch_givenHTTPResponse200_expectSetLocalMessageStoreFromFetchResponse() { +// XCTAssertTrue(manager.localMessageStore.isEmpty) +// +// setupHttpResponse(code: 200, body: sampleFetchResponseBody.data) +// manager.fetchUserMessages() +// +// XCTAssertEqual(manager.localMessageStore.count, 2) +// } +// +// func test_fetch_givenMessageCacheSaved_given304AfterSdkInitialized_expectPopulateLocalMessageStoreFromCache() { +// XCTAssertTrue(manager.localMessageStore.isEmpty) +// +// setupHttpResponse(code: 200, body: sampleFetchResponseBody.data) +// manager.fetchUserMessages() +// XCTAssertEqual(manager.localMessageStore.count, 2) +// +// let localMessageStoreBefore304: [Message] = manager.localMessageStore.values.compactMap { $0 } +// +// initializeManager() +// XCTAssertTrue(manager.localMessageStore.isEmpty) +// +// setupHttpResponse(code: 304, body: "".data) +// manager.fetchUserMessages() +// +// XCTAssertEqual(manager.localMessageStore.count, 2) +// let localMessageStoreAfter304 = manager.localMessageStore.values +// +// localMessageStoreBefore304.forEach { message in +// XCTAssertTrue(localMessageStoreAfter304.contains(message)) +// } +// } +// +// // The SDK could receive a 304 and the SDK does not have a previous fetch response cached. Example use cases when this could happen: +// // 1. The user logs out of the SDK and logs in again with same or different profile. +// // 2. Reinstalls the app and first fetch response is a 304 +// func test_fetch_givenNoPreviousCacheSaved_given304AfterSdkInitialized_expectPopulateLocalMessageStoreFromCache() { +// XCTAssertTrue(manager.localMessageStore.isEmpty) +// +// setupHttpResponse(code: 304, body: "".data) +// manager.fetchUserMessages() +// +// XCTAssertTrue(manager.localMessageStore.isEmpty) +// } +// } +// +// extension MessageQueueManagerIntegrationTests { +// // Convenient function for test functions that need to test when a new instance of manager is created (clearing in-memory stores). +// func initializeManager() { +// manager = MessageQueueManager() +// } +// } diff --git a/Tests/MessagingInApp/MessagingInAppImplementationTest.swift b/Tests/MessagingInApp/MessagingInAppImplementationTest.swift index 5d94ab81d..4e5e9bf61 100644 --- a/Tests/MessagingInApp/MessagingInAppImplementationTest.swift +++ b/Tests/MessagingInApp/MessagingInAppImplementationTest.swift @@ -14,20 +14,38 @@ class MessagingInAppImplementationTest: IntegrationTest { diGraphShared.eventBusHandler } - private let inAppProviderMock = InAppProviderMock() + private let gistProviderMock = GistProviderMock() private let eventListenerMock = InAppEventListenerMock() - private let eventBusHandlerMock = EventBusHandlerMock() + private let inAppMessageManagerMock = InAppMessageManagerMock() override func setUpDependencies() { super.setUpDependencies() - diGraphShared.override(value: inAppProviderMock, forType: InAppProvider.self) + diGraphShared.override(value: gistProviderMock, forType: GistProvider.self) + diGraphShared.override(value: inAppMessageManagerMock, forType: InAppMessageManager.self) } override func setUp() { + setupMocks() + // do not call super.setUp() because we want to initialize the module manually in test functions so we can test module being initialized. } + private func setupMocks() { + // Set up default return values for InAppMessageManagerMock + + inAppMessageManagerMock.dispatchReturnValue = Task {} + inAppMessageManagerMock.subscribeReturnValue = Task {} + + // Mock the dispatch method to fulfill the initialization expectation + inAppMessageManagerMock.dispatchClosure = { action, completion in + if case .initialize = action { + completion?() + } + return Task {} + } + } + // Function to call when test function is ready to initialize the SDK module. // // There are async tasks that can be performed after the module is initialized. @@ -56,19 +74,19 @@ class MessagingInAppImplementationTest: IntegrationTest { } let profileIdentifiedExpectation = createDefaultExpectation("Profile identified event to be received", expectProfileToIdentify) - inAppProviderMock.setProfileIdentifierClosure = { _ in + gistProviderMock.setUserTokenClosure = { _ in profileIdentifiedExpectation.fulfill() } combinedExpectations.append(profileIdentifiedExpectation) let sdkResetExpectation = createDefaultExpectation("SDK reset event to be received", expectSdkReset) - inAppProviderMock.clearIdentifyClosure = { + gistProviderMock.resetStateClosure = { sdkResetExpectation.fulfill() } combinedExpectations.append(sdkResetExpectation) let screenViewEventExpectation = createDefaultExpectation("Screen view event to be received", expectScreenViewEvent) - inAppProviderMock.setRouteClosure = { _ in + gistProviderMock.setCurrentRouteClosure = { _ in screenViewEventExpectation.fulfill() } combinedExpectations.append(screenViewEventExpectation) @@ -81,8 +99,20 @@ class MessagingInAppImplementationTest: IntegrationTest { func test_initialize_expectInitializeGistSDK() async { await waitForExpectations(initializeModule()) - XCTAssertTrue(inAppProviderMock.initializeCalled) - XCTAssertFalse(inAppProviderMock.setProfileIdentifierCalled) + inAppMessageManagerMock.dispatchReturnValue = Task {} + inAppMessageManagerMock.subscribeReturnValue = Task {} + + if let dispatchArgs = inAppMessageManagerMock.dispatchReceivedArguments?.action { + if case .initialize(let siteId, let dataCenter, let environment) = dispatchArgs { + XCTAssertEqual(siteId, messagingInAppConfigOptions.siteId) + XCTAssertEqual(dataCenter, messagingInAppConfigOptions.region.rawValue) + XCTAssertEqual(environment, GistEnvironment.production) + } else { + XCTFail("Expected dispatch action to be .initialize") + } + } else { + XCTFail("dispatchReceivedArguments is nil") + } } // MARK: initialize given an existing identifier @@ -94,8 +124,8 @@ class MessagingInAppImplementationTest: IntegrationTest { await waitForExpectations(initializeModule(expectProfileToIdentify: true)) - XCTAssertTrue(inAppProviderMock.setProfileIdentifierCalled) - XCTAssertEqual(inAppProviderMock.setProfileIdentifierReceivedArguments, givenProfileIdentifiedInSdk) + XCTAssertTrue(gistProviderMock.setUserTokenCalled) + XCTAssertEqual(gistProviderMock.setUserTokenReceivedArguments, givenProfileIdentifiedInSdk) } // MARK: profile hooks @@ -109,8 +139,8 @@ class MessagingInAppImplementationTest: IntegrationTest { await waitForExpectations(expectAsyncEventBusEvents) - XCTAssertEqual(inAppProviderMock.setProfileIdentifierCallsCount, 1) - XCTAssertEqual(inAppProviderMock.setProfileIdentifierReceivedArguments, given) + XCTAssertEqual(gistProviderMock.setUserTokenCallsCount, 1) + XCTAssertEqual(gistProviderMock.setUserTokenReceivedArguments, given) } func test_givenProfileNoLongerIdentified_expectRemoveFromInApp() async throws { @@ -121,7 +151,7 @@ class MessagingInAppImplementationTest: IntegrationTest { await waitForExpectations(expectAsyncEventBusEvents) - XCTAssertEqual(inAppProviderMock.clearIdentifyCallsCount, 1) + XCTAssertEqual(gistProviderMock.resetStateCallsCount, 1) } // MARK: screen view hooks @@ -135,171 +165,38 @@ class MessagingInAppImplementationTest: IntegrationTest { await waitForExpectations(expectAsyncEventBusEvents) - XCTAssertEqual(inAppProviderMock.setRouteCallsCount, 1) - XCTAssertEqual(inAppProviderMock.setRouteReceivedArguments, given) + XCTAssertEqual(gistProviderMock.setCurrentRouteCallsCount, 1) + XCTAssertEqual(gistProviderMock.setCurrentRouteReceivedArguments, given) } // MARK: event listeners - func test_eventListeners_expectCallListenerWithData() async { + func test_eventListeners_expectNoCallListenerWithData() async { await waitForExpectations(initializeModule()) - let givenGistMessage = Message.random - let expectedInAppMessage = InAppMessage(gistMessage: givenGistMessage) - messagingInApp.setEventListener(eventListenerMock) XCTAssertFalse(eventListenerMock.messageShownCalled) - messagingInApp.messageShown(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.messageShownCallsCount, 1) - XCTAssertEqual(eventListenerMock.messageShownReceivedArguments, expectedInAppMessage) // message dismissed XCTAssertFalse(eventListenerMock.messageDismissedCalled) - messagingInApp.messageDismissed(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.messageDismissedCallsCount, 1) - XCTAssertEqual(eventListenerMock.messageDismissedReceivedArguments, expectedInAppMessage) // error with message XCTAssertFalse(eventListenerMock.errorWithMessageCalled) - messagingInApp.messageError(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.errorWithMessageCallsCount, 1) - XCTAssertEqual(eventListenerMock.errorWithMessageReceivedArguments, expectedInAppMessage) // message action taken XCTAssertFalse(eventListenerMock.messageActionTakenCalled) - let givenCurrentRoute = String.random - let givenAction = String.random - let givenName = String.random - messagingInApp.action( - message: givenGistMessage, - currentRoute: givenCurrentRoute, - action: givenAction, - name: givenName - ) - XCTAssertEqual(eventListenerMock.messageActionTakenCallsCount, 1) - XCTAssertEqual(eventListenerMock.messageActionTakenReceivedArguments?.message, expectedInAppMessage) - XCTAssertEqual(eventListenerMock.messageActionTakenReceivedArguments?.actionValue, givenAction) - XCTAssertEqual(eventListenerMock.messageActionTakenReceivedArguments?.actionName, givenName) - } - - func test_eventListeners_expectCallListenerForEachEvent() async { - await waitForExpectations(initializeModule()) - - let givenGistMessage = Message.random - - messagingInApp.setEventListener(eventListenerMock) - - XCTAssertEqual(eventListenerMock.messageShownCallsCount, 0) - messagingInApp.messageShown(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.messageShownCallsCount, 1) - messagingInApp.messageShown(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.messageShownCallsCount, 2) - - // message dismissed - XCTAssertEqual(eventListenerMock.messageDismissedCallsCount, 0) - messagingInApp.messageDismissed(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.messageDismissedCallsCount, 1) - messagingInApp.messageDismissed(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.messageDismissedCallsCount, 2) - - // error with message - XCTAssertEqual(eventListenerMock.errorWithMessageCallsCount, 0) - messagingInApp.messageError(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.errorWithMessageCallsCount, 1) - messagingInApp.messageError(message: givenGistMessage) - XCTAssertEqual(eventListenerMock.errorWithMessageCallsCount, 2) - - // message action taken - XCTAssertEqual(eventListenerMock.messageActionTakenCallsCount, 0) - messagingInApp.action(message: givenGistMessage, currentRoute: .random, action: .random, name: .random) - XCTAssertEqual(eventListenerMock.messageActionTakenCallsCount, 1) - messagingInApp.action(message: givenGistMessage, currentRoute: .random, action: .random, name: .random) - XCTAssertEqual(eventListenerMock.messageActionTakenCallsCount, 2) - } - - func test_eventListeners_givenCloseAction_expectListenerEvent() async { - // override event bus handler to mock it so we can capture events - diGraphShared.override(value: eventBusHandlerMock, forType: EventBusHandler.self) - - await waitForExpectations(initializeModule()) - - let givenGistMessage = Message.random - let expectedInAppMessage = InAppMessage(gistMessage: givenGistMessage) - let givenCurrentRoute = String.random - let givenAction = "gist://close" - let givenName = String.random - - messagingInApp.setEventListener(eventListenerMock) - - XCTAssertEqual(eventListenerMock.messageActionTakenCallsCount, 0) - - messagingInApp.action( - message: givenGistMessage, - currentRoute: givenCurrentRoute, - action: givenAction, - name: givenName - ) - - XCTAssertEqual(eventListenerMock.messageActionTakenCallsCount, 1) - XCTAssertEqual(eventListenerMock.messageActionTakenReceivedArguments?.message, expectedInAppMessage) - XCTAssertEqual(eventListenerMock.messageActionTakenReceivedArguments?.actionValue, givenAction) - XCTAssertEqual(eventListenerMock.messageActionTakenReceivedArguments?.actionName, givenName) - - // make sure there is no click tracking for "close" action - XCTAssertEqual(eventBusHandlerMock.postEventCallsCount, 0) } - func test_inAppTracking_givenCustomAction_expectBQTrackInAppClicked() async { - // override event bus handler to mock it so we can capture events - diGraphShared.override(value: eventBusHandlerMock, forType: EventBusHandler.self) - - await waitForExpectations(initializeModule()) - - let givenGistMessage = Message.random - let expectedInAppMessage = InAppMessage(gistMessage: givenGistMessage) - let givenCurrentRoute = String.random - let givenAction = String.random - let givenName = String.random - let givenMetaData = ["actionName": givenName, "actionValue": givenAction] - - messagingInApp.action( - message: givenGistMessage, - currentRoute: givenCurrentRoute, - action: givenAction, - name: givenName - ) - - XCTAssertEqual(eventBusHandlerMock.postEventCallsCount, 1) - guard let postEventArgument = eventBusHandlerMock.postEventArguments as? TrackInAppMetricEvent else { - XCTFail("captured arguments must not be nil") - return - } - - XCTAssertEqual(postEventArgument.deliveryID, expectedInAppMessage.deliveryId) - XCTAssertEqual(postEventArgument.event, InAppMetric.clicked.rawValue) - XCTAssertEqual(postEventArgument.params, givenMetaData) - } + // MARK: dismiss message func test_dismissMessage_givenNoInAppMessage_expectNoError() async { await waitForExpectations(initializeModule()) // Dismiss in-app message - XCTAssertFalse(inAppProviderMock.dismissMessageCalled) - messagingInApp.dismissMessage() - XCTAssertEqual(inAppProviderMock.dismissMessageCallsCount, 1) - } - - func test_dismissMessage_givenInAppMessage_expectNoError() async { - await waitForExpectations(initializeModule()) - - let givenGistMessage = Message.random - _ = InAppMessage(gistMessage: givenGistMessage) - - // Dismiss in-app message when an in-app message is shown on screen - XCTAssertFalse(inAppProviderMock.dismissMessageCalled) + XCTAssertFalse(gistProviderMock.dismissMessageCalled) messagingInApp.dismissMessage() - XCTAssertEqual(inAppProviderMock.dismissMessageCallsCount, 1) + XCTAssertEqual(gistProviderMock.dismissMessageCallsCount, 1) } } diff --git a/Tests/MessagingInApp/MessagingInAppIntegrationTests.swift b/Tests/MessagingInApp/MessagingInAppIntegrationTests.swift index c6983f846..b668f59aa 100644 --- a/Tests/MessagingInApp/MessagingInAppIntegrationTests.swift +++ b/Tests/MessagingInApp/MessagingInAppIntegrationTests.swift @@ -1,304 +1,304 @@ -@testable import CioMessagingInApp -import Foundation -import SharedTests -import XCTest - -class MessagingInAppIntegrationTest: IntegrationTest { - private var engineWebProvider: EngineWebProvider { - EngineWebProviderStub(engineWebMock: engineWebMock) - } - - private let engineWebMock = EngineWebInstanceMock() - - private let globalEventListener = InAppEventListenerMock() - - override func setUp() { - super.setUp() - - // Setup mocks to return a non-empty value - engineWebMock.underlyingView = UIView() - - diGraphShared.override(value: engineWebProvider, forType: EngineWebProvider.self) - - // Important to test if global event listener gets called. Register one to test. - MessagingInApp.shared.setEventListener(globalEventListener) - } - - // MARK: Page rules and modal messages - - // When a customer adds page rules to a message, they expect that message to only be shown on that screen. - - func test_givenUserNavigatedToDifferentScreenWhileMessageLoading_expectDoNotShowModalMessage() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") - ] - - onDoneFetching(messages: givenMessages) - XCTAssertTrue(isCurrentlyLoadingMessage) - - navigateToScreen(screenName: "Settings") - XCTAssertFalse(isCurrentlyLoadingMessage) - - doneLoadingMessage(givenMessages[0]) - - XCTAssertNil(currentlyShownModalMessage) - XCTAssertFalse(didCallGlobalEventListener) - } - - func test_givenUserStillOnSameScreenAfterMessageLoads_expectShowModalMessage() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") - ] - onDoneFetching(messages: givenMessages) - XCTAssertTrue(isCurrentlyLoadingMessage) - - doneLoadingMessage(givenMessages[0]) - - XCTAssertNotNil(currentlyShownModalMessage) - XCTAssertFalse(didCallGlobalEventListener) - } - - func test_givenMessageHasNoPageRules_givenUserNavigatedToDifferentScreenWhileMessageLoaded_expectShowModalMessage() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(pageRule: nil) - ] - onDoneFetching(messages: givenMessages) - XCTAssertTrue(isCurrentlyLoadingMessage) - - navigateToScreen(screenName: "Settings") - XCTAssertTrue(isCurrentlyLoadingMessage) - - doneLoadingMessage(givenMessages[0]) - - XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[0].queueId) - XCTAssertFalse(didCallGlobalEventListener) - } - - func test_givenUserOnScreenDuringFetch_givenUserNavigatedToDifferentScreenWhileMessageLoading_expectShowModalMessageAfterGoBack() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") - ] - onDoneFetching(messages: givenMessages) - XCTAssertTrue(isCurrentlyLoadingMessage) - - navigateToScreen(screenName: "Settings") - XCTAssertFalse(isCurrentlyLoadingMessage) - - navigateToScreen(screenName: "Home") - XCTAssertTrue(isCurrentlyLoadingMessage) - - doneLoadingMessage(givenMessages[0]) - - XCTAssertNotNil(currentlyShownModalMessage) - XCTAssertFalse(didCallGlobalEventListener) - } - - func test_givenRouteChangedToSameRoute_expectDoNotDismissModal() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") - ] - onDoneFetching(messages: givenMessages) - - doneLoadingMessage(givenMessages[0]) - - XCTAssertNotNil(currentlyShownModalMessage) - - navigateToScreen(screenName: "Home") - - XCTAssertNotNil(currentlyShownModalMessage) - XCTAssertFalse(didCallGlobalEventListener) - } - - // page routes can contain regex which could make the message match the next screen navigated to. - func test_givenChangedRouteButMessageStillMatchesNewRoute_expectDoNotDismissModal() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(.*Home.*)$") - ] - onDoneFetching(messages: givenMessages) - - doneLoadingMessage(givenMessages[0]) - - XCTAssertNotNil(currentlyShownModalMessage) - - let messageShownBeforeNavigate = currentlyShownModalMessage - - navigateToScreen(screenName: "HomeSettings") - - XCTAssertNotNil(currentlyShownModalMessage) - - // because the message is identical, it was not canceled when the page route changed. - XCTAssertEqual(messageShownBeforeNavigate?.instanceId, currentlyShownModalMessage?.instanceId) - } - - // MARK: clearUserToken - - // Code that runs when the profile is logged out of the SDK - - func test_clearUserToken_givenModalMessageShown_givenModalHasPageRuleSet_expectDismissModal() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") - ] - onDoneFetching(messages: givenMessages) - doneLoadingMessage(givenMessages[0]) - XCTAssertNotNil(currentlyShownModalMessage) - - Gist.shared.clearUserToken() - - XCTAssertNil(currentlyShownModalMessage) - } - - func test_clearUserToken_givenModalMessageShown_givenModalHasNoPageRuleSet_expectDoNotDismissModal() { - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(messageId: "welcome-banner", campaignId: .random, pageRule: nil) - ] - onDoneFetching(messages: givenMessages) - doneLoadingMessage(givenMessages[0]) - XCTAssertNotNil(currentlyShownModalMessage) - - Gist.shared.clearUserToken() - - XCTAssertNotNil(currentlyShownModalMessage) - } - - // The in-app SDK maintains a cache of messages that are returned from the backend. When a profile is logged out of the SDK, we expect the message cache is cleared otherwise we run the risk of displaying messages meant for profile A to profile B. - func test_clearUserToken_givenProfileLoggedOutAndNewProfileLoggedIn_expectLocalMessageCacheCleared() { - Gist.shared.setUserToken("profile-A") - - XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) - setupHttpResponse(code: 200, body: readSampleDataFile(subdirectory: "InAppUserQueue", fileName: "fetch_response.json").data) - Gist.shared.messageQueueManager.fetchUserMessages() - XCTAssertFalse(Gist.shared.messageQueueManager.localMessageStore.isEmpty) - - // Expect no messages immediately after logging into another profile. - Gist.shared.clearUserToken() - XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) - Gist.shared.setUserToken("profile-B") - XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) - - // Expect that after first fetch with new profile logged in, the message cache remains empty. - setupHttpResponse(code: 304, body: "".data) - Gist.shared.messageQueueManager.fetchUserMessages() - XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) - } - - // MARK: action buttons - - func test_onCloseButton_expectShowNextMessageInQueue() throws { - // The test fails because it expects synchronous code, but there is async code. Another PR (https://github.com/customerio/customerio-ios/pull/738) makes tests synchronous. Once merged, we can remove this skip.") - try skipRunningTest() - - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(pageRule: "^(Home)$"), - Message(pageRule: "^(Home)$"), - Message(pageRule: nil) - ] - - onDoneFetching(messages: givenMessages) - doneLoadingMessage(givenMessages[0]) - XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[0].queueId) - - onCloseActionButtonPressed() - - doneLoadingMessage(givenMessages[1]) - - XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[1].queueId) - - onCloseActionButtonPressed() - - doneLoadingMessage(givenMessages[2]) - - XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[2].queueId) - - onCloseActionButtonPressed() - - XCTAssertNil(currentlyShownModalMessage) - } - - func test_onCloseButton_givenNextMessageDoesNotMatchPageRule_expectDoNotShowNextMessageInQueue() throws { - // The test fails because it expects synchronous code, but there is async code. Another PR (https://github.com/customerio/customerio-ios/pull/738) makes tests synchronous. Once merged, we can remove this skip.") - try skipRunningTest() - - navigateToScreen(screenName: "Home") - - let givenMessages = [ - Message(pageRule: "^(Home)$"), - Message(pageRule: "^(Settings)$") // expect to not show this message on close. - ] - - onDoneFetching(messages: givenMessages) - doneLoadingMessage(givenMessages[0]) - XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[0].queueId) - - onCloseActionButtonPressed() - - XCTAssertFalse(isCurrentlyLoadingMessage) // expect to not being loading a new message. - - navigateToScreen(screenName: "Settings") - - XCTAssertTrue(isCurrentlyLoadingMessage) // When page rule matches, we expect to load new message. - } -} - -extension MessagingInAppIntegrationTest { - var isCurrentlyLoadingMessage: Bool { - guard let messageManager = Gist.shared.getModalMessageManager() else { - return false // no modal message shown or loading - } - if messageManager.isShowingMessage { - return false // message already loaded. We want messages that are still loading. - } - - return true - } - - var currentlyShownModalMessage: Message? { - guard let messageManager = Gist.shared.getModalMessageManager() else { - return nil // no modal message shown or loading - } - if !messageManager.isShowingMessage { - return nil // message not loaded yet. - } - - return messageManager.currentMessage - } - - var didCallGlobalEventListener: Bool { - globalEventListener.messageDismissedCallsCount > 0 - } - - func onDoneFetching(messages: [Message]) { - Gist.shared.messageQueueManager.processFetchResponse(messages) - } - - func navigateToScreen(screenName: String) { - Gist.shared.setCurrentRoute(screenName) - } - - func doneLoadingMessage(_ message: Message) { - engineWebMock.underlyingDelegate?.routeLoaded(route: message.messageId) - } - - func onCloseActionButtonPressed() { - // Triggering the close button from the web engine simulates the user tapping the close button on the in-app WebView. - // This behaves more like an integration test because we are also able to test the message manager, too. - engineWebMock.underlyingDelegate?.tap(name: "", action: GistMessageActions.close.rawValue, system: false) - } -} +// @testable import CioMessagingInApp +// import Foundation +// import SharedTests +// import XCTest +// +// class MessagingInAppIntegrationTest: IntegrationTest { +// private var engineWebProvider: EngineWebProvider { +// EngineWebProviderStub(engineWebMock: engineWebMock) +// } +// +// private let engineWebMock = EngineWebInstanceMock() +// +// private let globalEventListener = InAppEventListenerMock() +// +// override func setUp() { +// super.setUp() +// +// // Setup mocks to return a non-empty value +// engineWebMock.underlyingView = UIView() +// +// diGraphShared.override(value: engineWebProvider, forType: EngineWebProvider.self) +// +// // Important to test if global event listener gets called. Register one to test. +// MessagingInApp.shared.setEventListener(globalEventListener) +// } +// +// // MARK: Page rules and modal messages +// +// // When a customer adds page rules to a message, they expect that message to only be shown on that screen. +// +// func test_givenUserNavigatedToDifferentScreenWhileMessageLoading_expectDoNotShowModalMessage() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") +// ] +// +// onDoneFetching(messages: givenMessages) +// XCTAssertTrue(isCurrentlyLoadingMessage) +// +// navigateToScreen(screenName: "Settings") +// XCTAssertFalse(isCurrentlyLoadingMessage) +// +// doneLoadingMessage(givenMessages[0]) +// +// XCTAssertNil(currentlyShownModalMessage) +// XCTAssertFalse(didCallGlobalEventListener) +// } +// +// func test_givenUserStillOnSameScreenAfterMessageLoads_expectShowModalMessage() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") +// ] +// onDoneFetching(messages: givenMessages) +// XCTAssertTrue(isCurrentlyLoadingMessage) +// +// doneLoadingMessage(givenMessages[0]) +// +// XCTAssertNotNil(currentlyShownModalMessage) +// XCTAssertFalse(didCallGlobalEventListener) +// } +// +// func test_givenMessageHasNoPageRules_givenUserNavigatedToDifferentScreenWhileMessageLoaded_expectShowModalMessage() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(pageRule: nil) +// ] +// onDoneFetching(messages: givenMessages) +// XCTAssertTrue(isCurrentlyLoadingMessage) +// +// navigateToScreen(screenName: "Settings") +// XCTAssertTrue(isCurrentlyLoadingMessage) +// +// doneLoadingMessage(givenMessages[0]) +// +// XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[0].queueId) +// XCTAssertFalse(didCallGlobalEventListener) +// } +// +// func test_givenUserOnScreenDuringFetch_givenUserNavigatedToDifferentScreenWhileMessageLoading_expectShowModalMessageAfterGoBack() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") +// ] +// onDoneFetching(messages: givenMessages) +// XCTAssertTrue(isCurrentlyLoadingMessage) +// +// navigateToScreen(screenName: "Settings") +// XCTAssertFalse(isCurrentlyLoadingMessage) +// +// navigateToScreen(screenName: "Home") +// XCTAssertTrue(isCurrentlyLoadingMessage) +// +// doneLoadingMessage(givenMessages[0]) +// +// XCTAssertNotNil(currentlyShownModalMessage) +// XCTAssertFalse(didCallGlobalEventListener) +// } +// +// func test_givenRouteChangedToSameRoute_expectDoNotDismissModal() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") +// ] +// onDoneFetching(messages: givenMessages) +// +// doneLoadingMessage(givenMessages[0]) +// +// XCTAssertNotNil(currentlyShownModalMessage) +// +// navigateToScreen(screenName: "Home") +// +// XCTAssertNotNil(currentlyShownModalMessage) +// XCTAssertFalse(didCallGlobalEventListener) +// } +// +// // page routes can contain regex which could make the message match the next screen navigated to. +// func test_givenChangedRouteButMessageStillMatchesNewRoute_expectDoNotDismissModal() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(.*Home.*)$") +// ] +// onDoneFetching(messages: givenMessages) +// +// doneLoadingMessage(givenMessages[0]) +// +// XCTAssertNotNil(currentlyShownModalMessage) +// +// let messageShownBeforeNavigate = currentlyShownModalMessage +// +// navigateToScreen(screenName: "HomeSettings") +// +// XCTAssertNotNil(currentlyShownModalMessage) +// +// // because the message is identical, it was not canceled when the page route changed. +// XCTAssertEqual(messageShownBeforeNavigate?.instanceId, currentlyShownModalMessage?.instanceId) +// } +// +// // MARK: clearUserToken +// +// // Code that runs when the profile is logged out of the SDK +// +// func test_clearUserToken_givenModalMessageShown_givenModalHasPageRuleSet_expectDismissModal() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(messageId: "welcome-banner", campaignId: .random, pageRule: "^(Home)$") +// ] +// onDoneFetching(messages: givenMessages) +// doneLoadingMessage(givenMessages[0]) +// XCTAssertNotNil(currentlyShownModalMessage) +// +// Gist.shared.clearUserToken() +// +// XCTAssertNil(currentlyShownModalMessage) +// } +// +// func test_clearUserToken_givenModalMessageShown_givenModalHasNoPageRuleSet_expectDoNotDismissModal() { +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(messageId: "welcome-banner", campaignId: .random, pageRule: nil) +// ] +// onDoneFetching(messages: givenMessages) +// doneLoadingMessage(givenMessages[0]) +// XCTAssertNotNil(currentlyShownModalMessage) +// +// Gist.shared.clearUserToken() +// +// XCTAssertNotNil(currentlyShownModalMessage) +// } +// +// // The in-app SDK maintains a cache of messages that are returned from the backend. When a profile is logged out of the SDK, we expect the message cache is cleared otherwise we run the risk of displaying messages meant for profile A to profile B. +// func test_clearUserToken_givenProfileLoggedOutAndNewProfileLoggedIn_expectLocalMessageCacheCleared() { +// Gist.shared.setUserToken("profile-A") +// +// XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) +// setupHttpResponse(code: 200, body: readSampleDataFile(subdirectory: "InAppUserQueue", fileName: "fetch_response.json").data) +// Gist.shared.messageQueueManager.fetchUserMessages() +// XCTAssertFalse(Gist.shared.messageQueueManager.localMessageStore.isEmpty) +// +// // Expect no messages immediately after logging into another profile. +// Gist.shared.clearUserToken() +// XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) +// Gist.shared.setUserToken("profile-B") +// XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) +// +// // Expect that after first fetch with new profile logged in, the message cache remains empty. +// setupHttpResponse(code: 304, body: "".data) +// Gist.shared.messageQueueManager.fetchUserMessages() +// XCTAssertTrue(Gist.shared.messageQueueManager.localMessageStore.isEmpty) +// } +// +// // MARK: action buttons +// +// func test_onCloseButton_expectShowNextMessageInQueue() throws { +// // The test fails because it expects synchronous code, but there is async code. Another PR (https://github.com/customerio/customerio-ios/pull/738) makes tests synchronous. Once merged, we can remove this skip.") +// try skipRunningTest() +// +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(pageRule: "^(Home)$"), +// Message(pageRule: "^(Home)$"), +// Message(pageRule: nil) +// ] +// +// onDoneFetching(messages: givenMessages) +// doneLoadingMessage(givenMessages[0]) +// XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[0].queueId) +// +// onCloseActionButtonPressed() +// +// doneLoadingMessage(givenMessages[1]) +// +// XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[1].queueId) +// +// onCloseActionButtonPressed() +// +// doneLoadingMessage(givenMessages[2]) +// +// XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[2].queueId) +// +// onCloseActionButtonPressed() +// +// XCTAssertNil(currentlyShownModalMessage) +// } +// +// func test_onCloseButton_givenNextMessageDoesNotMatchPageRule_expectDoNotShowNextMessageInQueue() throws { +// // The test fails because it expects synchronous code, but there is async code. Another PR (https://github.com/customerio/customerio-ios/pull/738) makes tests synchronous. Once merged, we can remove this skip.") +// try skipRunningTest() +// +// navigateToScreen(screenName: "Home") +// +// let givenMessages = [ +// Message(pageRule: "^(Home)$"), +// Message(pageRule: "^(Settings)$") // expect to not show this message on close. +// ] +// +// onDoneFetching(messages: givenMessages) +// doneLoadingMessage(givenMessages[0]) +// XCTAssertEqual(currentlyShownModalMessage?.queueId, givenMessages[0].queueId) +// +// onCloseActionButtonPressed() +// +// XCTAssertFalse(isCurrentlyLoadingMessage) // expect to not being loading a new message. +// +// navigateToScreen(screenName: "Settings") +// +// XCTAssertTrue(isCurrentlyLoadingMessage) // When page rule matches, we expect to load new message. +// } +// } +// +// extension MessagingInAppIntegrationTest { +// var isCurrentlyLoadingMessage: Bool { +// guard let messageManager = Gist.shared.getModalMessageManager() else { +// return false // no modal message shown or loading +// } +// if messageManager.isShowingMessage { +// return false // message already loaded. We want messages that are still loading. +// } +// +// return true +// } +// +// var currentlyShownModalMessage: Message? { +// guard let messageManager = Gist.shared.getModalMessageManager() else { +// return nil // no modal message shown or loading +// } +// if !messageManager.isShowingMessage { +// return nil // message not loaded yet. +// } +// +// return messageManager.currentMessage +// } +// +// var didCallGlobalEventListener: Bool { +// globalEventListener.messageDismissedCallsCount > 0 +// } +// +// func onDoneFetching(messages: [Message]) { +// Gist.shared.messageQueueManager.processFetchResponse(messages) +// } +// +// func navigateToScreen(screenName: String) { +// Gist.shared.setCurrentRoute(screenName) +// } +// +// func doneLoadingMessage(_ message: Message) { +// engineWebMock.underlyingDelegate?.routeLoaded(route: message.messageId) +// } +// +// func onCloseActionButtonPressed() { +// // Triggering the close button from the web engine simulates the user tapping the close button on the in-app WebView. +// // This behaves more like an integration test because we are also able to test the message manager, too. +// engineWebMock.underlyingDelegate?.tap(name: "", action: GistMessageActions.close.rawValue, system: false) +// } +// } diff --git a/Tests/MessagingInApp/State/InAppMessageStateTests.swift b/Tests/MessagingInApp/State/InAppMessageStateTests.swift index 96431d840..b9ec6cb17 100644 --- a/Tests/MessagingInApp/State/InAppMessageStateTests.swift +++ b/Tests/MessagingInApp/State/InAppMessageStateTests.swift @@ -1,3 +1,4 @@ +@testable import CioInternalCommon @testable import CioMessagingInApp import XCTest @@ -19,14 +20,28 @@ class InAppMessageStateTests: IntegrationTest { override func setUp() { super.setUp() engineWebMock.underlyingView = UIView() - diGraphShared.override(value: engineWebProvider, forType: EngineWebProvider.self) MessagingInApp.shared.setEventListener(globalEventListener) - inAppMessageManager = InAppMessageManager( + + diGraphShared.override(value: CioThreadUtil(), forType: ThreadUtil.self) + diGraphShared.override(value: engineWebProvider, forType: EngineWebProvider.self) + + inAppMessageManager = InAppMessageStoreManager( logger: diGraphShared.logger, threadUtil: diGraphShared.threadUtil, logManager: diGraphShared.logManager, gistDelegate: diGraphShared.gistDelegate ) + + diGraphShared.override(value: inAppMessageManager, forType: InAppMessageManager.self) + } + + // This add a wait so that all the middlewares are done processing by the time we check state + func dispatchAndWait(_ action: InAppMessageAction, timeout seconds: TimeInterval = 2) async throws { + let expectation = XCTestExpectation(description: "Action completed: \(action)") + inAppMessageManager.dispatch(action: action) { + expectation.fulfill() + } + await fulfillment(of: [expectation], timeout: seconds) } override func tearDown() { @@ -158,12 +173,12 @@ class InAppMessageStateTests: IntegrationTest { XCTAssertTrue(state.messagesInQueue.contains { $0.queueId == "2" }) } - func test_routeChange_givenMessageWithPageRule_expectMessageStateUpdated() async { + func test_routeChange_givenMessageWithPageRule_expectMessageStateUpdated() async throws { let message = Message(pageRule: "home", queueId: "1") await inAppMessageManager.dispatchAsync(action: .setUserIdentifier(user: .random)) await inAppMessageManager.dispatchAsync(action: .processMessageQueue(messages: [message])) - await inAppMessageManager.dispatchAsync(action: .setPageRoute(route: "home")) + try await dispatchAndWait(.setPageRoute(route: "home")) var state = await inAppMessageManager.state XCTAssertEqual(state.currentRoute, "home") @@ -175,7 +190,7 @@ class InAppMessageStateTests: IntegrationTest { } // Change route to dismiss the message - await inAppMessageManager.dispatchAsync(action: .setPageRoute(route: "profile")) + try await dispatchAndWait(.setPageRoute(route: "profile")) state = await inAppMessageManager.state XCTAssertEqual(state.currentRoute, "profile") @@ -268,7 +283,7 @@ class InAppMessageStateTests: IntegrationTest { XCTAssertFalse(globalEventListener.messageActionTakenCalled) } - func test_routeChange_expectMessageProcessing() async { + func test_routeChange_expectMessageProcessing() async throws { let message1 = Message(pageRule: "home", queueId: "1") let message2 = Message(pageRule: "profile", queueId: "2") @@ -276,7 +291,7 @@ class InAppMessageStateTests: IntegrationTest { await inAppMessageManager.dispatchAsync(action: .processMessageQueue(messages: [message1, message2])) // Set route to "home" - await inAppMessageManager.dispatchAsync(action: .setPageRoute(route: "home")) + try await dispatchAndWait(.setPageRoute(route: "home")) var state = await inAppMessageManager.state XCTAssertEqual(state.currentRoute, "home") @@ -288,7 +303,7 @@ class InAppMessageStateTests: IntegrationTest { } // Change route to "profile" - await inAppMessageManager.dispatchAsync(action: .setPageRoute(route: "profile")) + try await dispatchAndWait(.setPageRoute(route: "profile")) state = await inAppMessageManager.state XCTAssertEqual(state.currentRoute, "profile") @@ -300,7 +315,7 @@ class InAppMessageStateTests: IntegrationTest { } } - func test_routeChange_givenMessageBeingProcessed_expectMessageHandledCorrectly() async { + func test_routeChange_givenMessageBeingProcessed_expectMessageHandledCorrectly() async throws { let message = Message(pageRule: "home", queueId: "1") await inAppMessageManager.dispatchAsync(action: .setUserIdentifier(user: .random)) @@ -310,13 +325,13 @@ class InAppMessageStateTests: IntegrationTest { var state = await inAppMessageManager.state XCTAssertEqual(state.currentMessageState, .loading(message: message)) - await inAppMessageManager.dispatchAsync(action: .setPageRoute(route: "profile")) + try await dispatchAndWait(.setPageRoute(route: "profile")) state = await inAppMessageManager.state XCTAssertEqual(state.currentMessageState, .dismissed(message: message)) XCTAssertEqual(state.currentRoute, "profile") - await inAppMessageManager.dispatchAsync(action: .setPageRoute(route: "home")) + try await dispatchAndWait(.setPageRoute(route: "home")) state = await inAppMessageManager.state XCTAssertEqual(state.currentMessageState, .loading(message: message))