diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 52ac21132..4d2167cce 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -4,10 +4,10 @@ on: [pull_request] jobs: SwiftLint: - runs-on: ubuntu-latest + runs-on: macos-latest steps: - uses: actions/checkout@v2 - - name: Lint via SwiftLint - uses: norio-nomura/action-swiftlint@3.2.1 - with: - args: --strict \ No newline at end of file + - name: Install swiftlint + run: brew install swiftlint || brew upgrade swiftlint + - name: Run swiftlint. Fail if any errors. + run: make lint \ No newline at end of file diff --git a/Package.resolved b/Package.resolved index 5d7c7ac57..85ff9d73a 100644 --- a/Package.resolved +++ b/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://gitlab.com/bourbonltd/gist-apple.git", "state": { "branch": null, - "revision": "2b1f647d96038027c3018e9bfff6cb27e944bf09", - "version": "2.2.1" + "revision": "408077c633807dc00fbce263cf513f69ab40a75b", + "version": "2.2.2" } } ] diff --git a/Sources/Common/Util/Log.swift b/Sources/Common/Util/Log.swift index 83c574a6f..28705aa99 100644 --- a/Sources/Common/Util/Log.swift +++ b/Sources/Common/Util/Log.swift @@ -28,7 +28,7 @@ public enum CioLogLevel: String, CaseIterable { case debug #if canImport(os) - func shouldLog(_ level: OSLogType) -> Bool { + func shouldLog(_ level: CioLogLevel) -> Bool { switch self { case .none: return false case .error: @@ -39,6 +39,15 @@ public enum CioLogLevel: String, CaseIterable { return true } } + + var osLogLevel: OSLogType { + switch self { + case .none: return .info + case .error: return .error + case .info: return .info + case .debug: return .debug + } + } #endif } @@ -46,8 +55,8 @@ public enum CioLogLevel: String, CaseIterable { // sourcery: InjectRegister = "Logger" public class ConsoleLogger: Logger { // allows filtering in Console mac app - private let logSubsystem = "io.customer.sdk" - private let logCategory = "CIO" + public static let logSubsystem = "io.customer.sdk" + public static let logCategory = "CIO" private let siteId: SiteId private let sdkConfig: SdkConfig @@ -61,21 +70,12 @@ public class ConsoleLogger: Logger { self.sdkConfig = sdkConfig } - #if canImport(os) - // Unified logging for Swift. https://www.avanderlee.com/workflow/oslog-unified-logging/ - // This means we can view logs in xcode console + Console app. - private func printMessage(_ message: String, _ level: OSLogType) { + private func printMessage(_ message: String, _ level: CioLogLevel) { if !minLogLevel.shouldLog(level) { return } let messageToPrint = "(siteid:\(siteId.abbreviatedSiteId)) \(message)" - if #available(iOS 14, *) { - let logger = os.Logger(subsystem: self.logSubsystem, category: self.logCategory) - logger.log(level: level, "\(messageToPrint, privacy: .public)") - } else { - let logger = OSLog(subsystem: logSubsystem, category: logCategory) - os_log("%{public}@", log: logger, type: level, messageToPrint) - } + ConsoleLogger.logMessageToConsole(messageToPrint, level: level) } public func debug(_ message: String) { @@ -89,14 +89,33 @@ public class ConsoleLogger: Logger { public func error(_ message: String) { printMessage("🛑 \(message)", .error) } - #else - // At this time, Linux cannot use `os.log` or `OSLog`. Instead, use: https://github.com/apple/swift-log/ - // As we don't officially support Linux at this time, no need to add a dependency to the project. - // therefore, we are not logging if can't import os.log - public func debug(_ message: String) {} - public func info(_ message: String) {} - public func error(_ message: String) {} - #endif + + public static func logMessageToConsole(_ message: String, level: CioLogLevel) { + #if canImport(os) + // Unified logging for Swift. https://www.avanderlee.com/workflow/oslog-unified-logging/ + // This means we can view logs in xcode console + Console app. + if #available(iOS 14, *) { + let logger = os.Logger(subsystem: self.logSubsystem, category: self.logCategory) + logger.log(level: level.osLogLevel, "\(message, privacy: .public)") + } else { + let logger = OSLog(subsystem: logSubsystem, category: logCategory) + os_log("%{public}@", log: logger, type: level.osLogLevel, message) + } + #else + // At this time, Linux cannot use `os.log` or `OSLog`. Instead, use: https://github.com/apple/swift-log/ + // As we don't officially support Linux at this time, no need to add a dependency to the project. + // therefore, we are not logging if can't import os.log + #endif + } +} + +// Log messages to customers when the SDK is not initialized. +// Great for alerts when the SDK may not be setup correctly. +// Since the SDK is not initialized, dependencies graph is not created. +// Therefore, this is just a function that's available at the top-level available to all +// of the SDK without the use of dependencies. +public func sdkNotInitializedAlert(_ message: String) { + ConsoleLogger.logMessageToConsole("⚠️ \(message)", level: .error) } extension CioLogLevel { diff --git a/Sources/MessagingInApp/MessagingInApp.swift b/Sources/MessagingInApp/MessagingInApp.swift index f1f982f71..e7e9acf22 100644 --- a/Sources/MessagingInApp/MessagingInApp.swift +++ b/Sources/MessagingInApp/MessagingInApp.swift @@ -5,13 +5,14 @@ import Gist public protocol MessagingInAppInstance: AutoMockable { func initialize(organizationId: String) + // sourcery:Name=initializeEventListener + func initialize(organizationId: String, eventListener: InAppEventListener) } public class MessagingInApp: ModuleTopLevelObject, MessagingInAppInstance { @Atomic public private(set) static var shared = MessagingInApp() - // testing constructor - override internal init(implementation: MessagingInAppInstance, sdkInitializedUtil: SdkInitializedUtil) { + override internal init(implementation: MessagingInAppInstance?, sdkInitializedUtil: SdkInitializedUtil) { super.init(implementation: implementation, sdkInitializedUtil: sdkInitializedUtil) } @@ -25,27 +26,68 @@ public class MessagingInApp: ModuleTopLevelObject, Messa Self.shared = MessagingInApp() } - // Initialize SDK module + // testing constructor + internal static func initialize(organizationId: String, eventListener: InAppEventListener?, implementation: MessagingInAppInstance?, sdkInitializedUtil: SdkInitializedUtil) { + Self.shared = MessagingInApp(implementation: implementation, sdkInitializedUtil: sdkInitializedUtil) + + if let eventListener = eventListener { + Self.initialize(organizationId: organizationId, eventListener: eventListener) + } else { + Self.initialize(organizationId: organizationId) + } + } + + // MARK: static initialized functions for customers. + + // static functions are identical to initialize functions in InAppInstance protocol to try and make a more convenient + // API for customers. Customers can use `MessagingInApp.initialize(...)` instead of `MessagingInApp.shared.initialize(...)`. + // Trying to follow the same API as `CustomerIO` class with `initialize()`. + public static func initialize(organizationId: String) { Self.shared.initialize(organizationId: organizationId) } + public static func initialize(organizationId: String, eventListener: InAppEventListener) { + Self.shared.initialize(organizationId: organizationId, eventListener: eventListener) + } + + // MARK: initialize functions to initialize module. + + // Multiple initialize functions to inherit the InAppInstance protocol which contains multiple initialize functions. + public func initialize(organizationId: String) { - initialize() // enables features such as setting up hooks + commonInitialize(organizationId: organizationId, eventListener: nil) + } + + public func initialize(organizationId: String, eventListener: InAppEventListener) { + commonInitialize(organizationId: organizationId, eventListener: eventListener) + } + + private func commonInitialize(organizationId: String, eventListener: InAppEventListener?) { + guard let implementation = implementation else { + sdkNotInitializedAlert("CustomerIO class has not yet been initialized. Request to initialize the in-app module has been ignored.") + return + } + + initialize() - implementation?.initialize(organizationId: organizationId) + if let eventListener = eventListener { + implementation.initialize(organizationId: organizationId, eventListener: eventListener) + } else { + implementation.initialize(organizationId: organizationId) + } } override public func inititlize(diGraph: DIGraph) { let logger = diGraph.logger - logger.debug("Setting up MessagingInApp module...") + logger.debug("Setting up in-app module...") // Register MessagingPush module hooks now that the module is being initialized. let hooks = diGraph.hooksManager let moduleHookProvider = MessagingInAppModuleHookProvider() hooks.add(key: .messagingInApp, provider: moduleHookProvider) - logger.info("MessagingInApp module setup with SDK") + logger.info("In-app module setup with SDK") } override public func getImplementationInstance(diGraph: DIGraph) -> MessagingInAppInstance { diff --git a/Sources/MessagingInApp/MessagingInAppImplementation.swift b/Sources/MessagingInApp/MessagingInAppImplementation.swift index c1b3467df..bd949e908 100644 --- a/Sources/MessagingInApp/MessagingInAppImplementation.swift +++ b/Sources/MessagingInApp/MessagingInAppImplementation.swift @@ -8,18 +8,31 @@ internal class MessagingInAppImplementation: MessagingInAppInstance { private var queue: Queue private var jsonAdapter: JsonAdapter private var inAppProvider: InAppProvider + private var profileStore: ProfileStore + + private var eventListener: InAppEventListener? init(diGraph: DIGraph) { self.logger = diGraph.logger self.queue = diGraph.queue self.jsonAdapter = diGraph.jsonAdapter self.inAppProvider = diGraph.inAppProvider + self.profileStore = diGraph.profileStore } func initialize(organizationId: String) { - logger.debug("gist SDK being setup \(organizationId)") - inAppProvider.initialize(organizationId: organizationId, delegate: self) + + // if identifier is already present, set the userToken again so in case if the customer was already identified and + // module was added later on, we can notify gist about it. + if let identifier = profileStore.identifier { + inAppProvider.setProfileIdentifier(identifier) + } + } + + func initialize(organizationId: String, eventListener: InAppEventListener) { + self.eventListener = eventListener + initialize(organizationId: organizationId) } } @@ -54,6 +67,8 @@ extension MessagingInAppImplementation: GistDelegate { public func messageShown(message: Message) { logger.debug("in-app message opened. \(message.describeForLogs)") + eventListener?.messageShown(message: InAppMessage(gistMessage: message)) + if let deliveryId = getDeliveryId(from: message) { // the state of the SDK does not change if adding this queue task isn't successful so ignore result _ = queue.addTrackInAppDeliveryTask(deliveryId: deliveryId, event: .opened) @@ -62,10 +77,14 @@ extension MessagingInAppImplementation: GistDelegate { public func messageDismissed(message: Message) { logger.debug("in-app message dismissed. \(message.describeForLogs)") + + eventListener?.messageDismissed(message: InAppMessage(gistMessage: message)) } public func messageError(message: Message) { logger.error("error with in-app message. \(message.describeForLogs)") + + eventListener?.errorWithMessage(message: InAppMessage(gistMessage: message)) } public func action(message: Message, currentRoute: String, action: String, name: String) { @@ -80,6 +99,12 @@ extension MessagingInAppImplementation: GistDelegate { // the state of the SDK does not change if adding this queue task isn't successful so ignore result _ = queue.addTrackInAppDeliveryTask(deliveryId: deliveryId, event: .clicked) } + + eventListener?.messageActionTaken( + message: InAppMessage(gistMessage: message), + action: action, + name: name + ) } private func getDeliveryId(from message: Message) -> String? { diff --git a/Sources/MessagingInApp/Type/InAppEventListener.swift b/Sources/MessagingInApp/Type/InAppEventListener.swift new file mode 100644 index 000000000..f49d69412 --- /dev/null +++ b/Sources/MessagingInApp/Type/InAppEventListener.swift @@ -0,0 +1,10 @@ +import Common +import Foundation +import Gist + +public protocol InAppEventListener: AutoMockable { + func messageShown(message: InAppMessage) + func messageDismissed(message: InAppMessage) + func errorWithMessage(message: InAppMessage) + func messageActionTaken(message: InAppMessage, action: String, name: String) +} diff --git a/Sources/MessagingInApp/Type/InAppMessage.swift b/Sources/MessagingInApp/Type/InAppMessage.swift new file mode 100644 index 000000000..75ba9979f --- /dev/null +++ b/Sources/MessagingInApp/Type/InAppMessage.swift @@ -0,0 +1,15 @@ +import Foundation +import Gist + +typealias GistMessage = Message + +public struct InAppMessage: Equatable { + public let messageId: String + public let deliveryId: String? // (Currently taken from Gist's campaignId property). Can be nil when sending test + // in-app messages + + internal init(gistMessage: GistMessage) { + self.messageId = gistMessage.messageId + self.deliveryId = gistMessage.gistProperties.campaignId + } +} diff --git a/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift b/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift index 18c6c482f..dc1cf74b0 100644 --- a/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift +++ b/Sources/MessagingInApp/autogenerated/AutoMockable.generated.swift @@ -82,6 +82,153 @@ import Gist */ +/** + Class to easily create a mocked version of the `InAppEventListener` class. + This class is equipped with functions and properties ready for you to mock! + + Note: This file is automatically generated. This means the mocks should always be up-to-date and has a consistent API. + See the SDK documentation to learn the basics behind using the mock classes in the SDK. + */ +public class InAppEventListenerMock: InAppEventListener, Mock { + /// If *any* interactions done on mock. `true` if any method or property getter/setter called. + public var mockCalled: Bool = false // + + public init() { + Mocks.shared.add(mock: self) + } + + public func resetMock() { + messageShownCallsCount = 0 + messageShownReceivedArguments = nil + messageShownReceivedInvocations = [] + + mockCalled = false // do last as resetting properties above can make this true + messageDismissedCallsCount = 0 + messageDismissedReceivedArguments = nil + messageDismissedReceivedInvocations = [] + + mockCalled = false // do last as resetting properties above can make this true + errorWithMessageCallsCount = 0 + errorWithMessageReceivedArguments = nil + errorWithMessageReceivedInvocations = [] + + mockCalled = false // do last as resetting properties above can make this true + messageActionTakenCallsCount = 0 + messageActionTakenReceivedArguments = nil + messageActionTakenReceivedInvocations = [] + + mockCalled = false // do last as resetting properties above can make this true + } + + // MARK: - messageShown + + /// Number of times the function was called. + public private(set) var messageShownCallsCount = 0 + /// `true` if the function was ever called. + public var messageShownCalled: Bool { + messageShownCallsCount > 0 + } + + /// The arguments from the *last* time the function was called. + public private(set) var messageShownReceivedArguments: InAppMessage? + /// Arguments from *all* of the times that the function was called. + public private(set) var messageShownReceivedInvocations: [InAppMessage] = [] + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + */ + public var messageShownClosure: ((InAppMessage) -> Void)? + + /// Mocked function for `messageShown(message: InAppMessage)`. Your opportunity to return a mocked value and check result of mock in test code. + public func messageShown(message: InAppMessage) { + mockCalled = true + messageShownCallsCount += 1 + messageShownReceivedArguments = message + messageShownReceivedInvocations.append(message) + messageShownClosure?(message) + } + + // MARK: - messageDismissed + + /// Number of times the function was called. + public private(set) var messageDismissedCallsCount = 0 + /// `true` if the function was ever called. + public var messageDismissedCalled: Bool { + messageDismissedCallsCount > 0 + } + + /// The arguments from the *last* time the function was called. + public private(set) var messageDismissedReceivedArguments: InAppMessage? + /// Arguments from *all* of the times that the function was called. + public private(set) var messageDismissedReceivedInvocations: [InAppMessage] = [] + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + */ + public var messageDismissedClosure: ((InAppMessage) -> Void)? + + /// Mocked function for `messageDismissed(message: InAppMessage)`. Your opportunity to return a mocked value and check result of mock in test code. + public func messageDismissed(message: InAppMessage) { + mockCalled = true + messageDismissedCallsCount += 1 + messageDismissedReceivedArguments = message + messageDismissedReceivedInvocations.append(message) + messageDismissedClosure?(message) + } + + // MARK: - errorWithMessage + + /// Number of times the function was called. + public private(set) var errorWithMessageCallsCount = 0 + /// `true` if the function was ever called. + public var errorWithMessageCalled: Bool { + errorWithMessageCallsCount > 0 + } + + /// The arguments from the *last* time the function was called. + public private(set) var errorWithMessageReceivedArguments: InAppMessage? + /// Arguments from *all* of the times that the function was called. + public private(set) var errorWithMessageReceivedInvocations: [InAppMessage] = [] + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + */ + public var errorWithMessageClosure: ((InAppMessage) -> Void)? + + /// Mocked function for `errorWithMessage(message: InAppMessage)`. Your opportunity to return a mocked value and check result of mock in test code. + public func errorWithMessage(message: InAppMessage) { + mockCalled = true + errorWithMessageCallsCount += 1 + errorWithMessageReceivedArguments = message + errorWithMessageReceivedInvocations.append(message) + errorWithMessageClosure?(message) + } + + // MARK: - messageActionTaken + + /// Number of times the function was called. + public private(set) var messageActionTakenCallsCount = 0 + /// `true` if the function was ever called. + public var messageActionTakenCalled: Bool { + messageActionTakenCallsCount > 0 + } + + /// The arguments from the *last* time the function was called. + public private(set) var messageActionTakenReceivedArguments: (message: InAppMessage, action: String, name: String)? + /// Arguments from *all* of the times that the function was called. + public private(set) var messageActionTakenReceivedInvocations: [(message: InAppMessage, action: String, name: String)] = [] + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + */ + public var messageActionTakenClosure: ((InAppMessage, String, String) -> Void)? + + /// Mocked function for `messageActionTaken(message: InAppMessage, action: String, name: String)`. Your opportunity to return a mocked value and check result of mock in test code. + public func messageActionTaken(message: InAppMessage, action: String, name: String) { + mockCalled = true + messageActionTakenCallsCount += 1 + messageActionTakenReceivedArguments = (message: message, action: action, name: name) + messageActionTakenReceivedInvocations.append((message: message, action: action, name: name)) + messageActionTakenClosure?(message, action, name) + } +} + /** Class to easily create a mocked version of the `InAppProvider` class. This class is equipped with functions and properties ready for you to mock! @@ -241,6 +388,11 @@ public class MessagingInAppInstanceMock: MessagingInAppInstance, Mock { initializeReceivedArguments = nil initializeReceivedInvocations = [] + mockCalled = false // do last as resetting properties above can make this true + initializeEventListenerCallsCount = 0 + initializeEventListenerReceivedArguments = nil + initializeEventListenerReceivedInvocations = [] + mockCalled = false // do last as resetting properties above can make this true } @@ -270,4 +422,31 @@ public class MessagingInAppInstanceMock: MessagingInAppInstance, Mock { initializeReceivedInvocations.append(organizationId) initializeClosure?(organizationId) } + + // MARK: - initialize + + /// Number of times the function was called. + public private(set) var initializeEventListenerCallsCount = 0 + /// `true` if the function was ever called. + public var initializeEventListenerCalled: Bool { + initializeEventListenerCallsCount > 0 + } + + /// The arguments from the *last* time the function was called. + public private(set) var initializeEventListenerReceivedArguments: (organizationId: String, eventListener: InAppEventListener)? + /// Arguments from *all* of the times that the function was called. + public private(set) var initializeEventListenerReceivedInvocations: [(organizationId: String, eventListener: InAppEventListener)] = [] + /** + Set closure to get called when function gets called. Great way to test logic or return a value for the function. + */ + public var initializeEventListenerClosure: ((String, InAppEventListener) -> Void)? + + /// Mocked function for `initialize(organizationId: String, eventListener: InAppEventListener)`. Your opportunity to return a mocked value and check result of mock in test code. + public func initialize(organizationId: String, eventListener: InAppEventListener) { + mockCalled = true + initializeEventListenerCallsCount += 1 + initializeEventListenerReceivedArguments = (organizationId: organizationId, eventListener: eventListener) + initializeEventListenerReceivedInvocations.append((organizationId: organizationId, eventListener: eventListener)) + initializeEventListenerClosure?(organizationId, eventListener) + } } diff --git a/Sources/MessagingPush/MessagingPush.swift b/Sources/MessagingPush/MessagingPush.swift index 49c562e89..00f233875 100644 --- a/Sources/MessagingPush/MessagingPush.swift +++ b/Sources/MessagingPush/MessagingPush.swift @@ -14,7 +14,7 @@ public class MessagingPush: ModuleTopLevelObject, Messagi @Atomic public private(set) static var shared = MessagingPush() // testing constructor - override internal init(implementation: MessagingPushInstance, sdkInitializedUtil: SdkInitializedUtil) { + override internal init(implementation: MessagingPushInstance?, sdkInitializedUtil: SdkInitializedUtil) { super.init(implementation: implementation, sdkInitializedUtil: sdkInitializedUtil) } diff --git a/Sources/Tracking/ModuleTopLevelObject.swift b/Sources/Tracking/ModuleTopLevelObject.swift index 7b4a8c108..41d1f1009 100644 --- a/Sources/Tracking/ModuleTopLevelObject.swift +++ b/Sources/Tracking/ModuleTopLevelObject.swift @@ -17,7 +17,8 @@ open class ModuleTopLevelObject { private let sdkInitializedUtil: SdkInitializedUtil // for writing tests - public init(implementation: ImplementationClass, sdkInitializedUtil: SdkInitializedUtil) { + // provide a nil implementation if you want `sdkInitializedUtil` logic to run and real instance of implementation to run in tests + public init(implementation: ImplementationClass?, sdkInitializedUtil: SdkInitializedUtil) { self.alreadyCreatedImplementation = implementation self.sdkInitializedUtil = sdkInitializedUtil } diff --git a/Tests/MessagingInApp/APITest.swift b/Tests/MessagingInApp/APITest.swift index 27d639fe9..1aa0f236a 100644 --- a/Tests/MessagingInApp/APITest.swift +++ b/Tests/MessagingInApp/APITest.swift @@ -18,7 +18,23 @@ class MessagingInAppAPITest: UnitTest { func test_allPublicFunctions() throws { try skipRunningTest() - MessagingInApp.shared.initialize(organizationId: "") + MessagingInApp.initialize(organizationId: "") + MessagingInApp.initialize(organizationId: "", eventListener: self) mock.initialize(organizationId: "") + mock.initialize(organizationId: "", eventListener: self) } } + +extension MessagingInAppAPITest: InAppEventListener { + func messageShown(message: InAppMessage) { + // make sure all properties of InAppMessage are accessible + _ = message.messageId + _ = message.deliveryId + } + + func messageDismissed(message: InAppMessage) {} + + func errorWithMessage(message: InAppMessage) {} + + func messageActionTaken(message: InAppMessage, action: String, name: String) {} +} diff --git a/Tests/MessagingInApp/Extensions/GistExtensions.swift b/Tests/MessagingInApp/Extensions/GistExtensions.swift new file mode 100644 index 000000000..cc851cf46 --- /dev/null +++ b/Tests/MessagingInApp/Extensions/GistExtensions.swift @@ -0,0 +1,19 @@ +@testable import CioMessagingInApp +import Foundation +@testable import Gist + +extension Message { + convenience init(messageId: String, campaignId: String) { + let gistProperties = [ + "gist": [ + "campaignId": campaignId + ] + ] + + self.init(messageId: messageId, properties: gistProperties) + } + + static var random: Message { + Message(messageId: .random, campaignId: .random) + } +} diff --git a/Tests/MessagingInApp/MessagingInAppImplementationTest.swift b/Tests/MessagingInApp/MessagingInAppImplementationTest.swift index 494c2812d..1e509fb4b 100644 --- a/Tests/MessagingInApp/MessagingInAppImplementationTest.swift +++ b/Tests/MessagingInApp/MessagingInAppImplementationTest.swift @@ -1,5 +1,6 @@ @testable import CioMessagingInApp @testable import CioTracking +@testable import Common import Foundation import Gist import SharedTests @@ -9,13 +10,17 @@ class MessagingInAppImplementationTest: UnitTest { private var messagingInApp: MessagingInAppImplementation! private let inAppProviderMock = InAppProviderMock() + private let eventListenerMock = InAppEventListenerMock() + private let profileStoreMock = ProfileStoreMock() override func setUp() { super.setUp() diGraph.override(value: inAppProviderMock, forType: InAppProvider.self) + diGraph.override(value: profileStoreMock, forType: ProfileStore.self) messagingInApp = MessagingInAppImplementation(diGraph: diGraph) + messagingInApp.initialize(organizationId: .random, eventListener: eventListenerMock) } // MARK: initialize @@ -23,7 +28,31 @@ class MessagingInAppImplementationTest: UnitTest { func test_initialize_givenOrganizationId_expectInitializeGistSDK() { let givenId = String.random - messagingInApp.initialize(organizationId: givenId) + let instance = MessagingInAppImplementation(diGraph: diGraph) + instance.initialize(organizationId: givenId) + + XCTAssertTrue(inAppProviderMock.initializeCalled) + } + + func test_initialize_givenIdentifier_expectGistSetProfileIdentifier() { + let givenProfileIdentifiedInSdk = String.random + + profileStoreMock.identifier = givenProfileIdentifiedInSdk + + let givenId = String.random + let instance = MessagingInAppImplementation(diGraph: diGraph) + instance.initialize(organizationId: givenId) + + XCTAssertTrue(inAppProviderMock.initializeCalled) + XCTAssertTrue(inAppProviderMock.setProfileIdentifierCalled) + XCTAssertEqual(inAppProviderMock.setProfileIdentifierReceivedArguments, givenProfileIdentifiedInSdk) + } + + func test_initialize_givenOrganizationId_givenEventListener_expectInitializeGistSDK() { + let givenId = String.random + + let instance = MessagingInAppImplementation(diGraph: diGraph) + instance.initialize(organizationId: givenId, eventListener: eventListenerMock) XCTAssertTrue(inAppProviderMock.initializeCalled) } @@ -55,4 +84,77 @@ class MessagingInAppImplementationTest: UnitTest { XCTAssertEqual(inAppProviderMock.setRouteCallsCount, 1) XCTAssertEqual(inAppProviderMock.setRouteReceivedArguments, given) } + + // MARK: event listeners + + func test_eventListeners_expectCallListenerWithData() { + let givenGistMessage = Message.random + let expectedInAppMessage = InAppMessage(gistMessage: givenGistMessage) + + // Message opened + 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?.action, givenAction) + XCTAssertEqual(eventListenerMock.messageActionTakenReceivedArguments?.name, givenName) + } + + func test_eventListeners_expectCallListenerForEachEvent() { + let givenGistMessage = Message.random + + // Message opened + 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) + } } diff --git a/Tests/MessagingInApp/MessagingInAppTest.swift b/Tests/MessagingInApp/MessagingInAppTest.swift index 080589731..3ca8d017e 100644 --- a/Tests/MessagingInApp/MessagingInAppTest.swift +++ b/Tests/MessagingInApp/MessagingInAppTest.swift @@ -1,24 +1,52 @@ @testable import CioMessagingInApp -import Common +@testable import CioTracking +@testable import Common import Foundation +@testable import Gist import SharedTests import XCTest -class MessagingInAppTest: IntegrationTest { +class MessagingInAppTest: UnitTest { private let hooksMock = HooksManagerMock() + private let implementationMock = MessagingInAppInstanceMock() + private let sdkInitializedUtilMock = SdkInitializedUtilMock() override func setUp() { super.setUp() MessagingInApp.resetSharedInstance() + // This is where we inject the DI graph into our tests + sdkInitializedUtilMock.underlyingPostInitializedData = (siteId: testSiteId, diGraph: diGraph) + diGraph.override(value: hooksMock, forType: HooksManager.self) } - func test_initializeWithOrganizationId_expectCallModuleInitializeCode() { - MessagingInApp.initialize(organizationId: String.random) + func test_initialize_noEventListener_expectCallModuleInitializeCode() { + MessagingInApp.initialize(organizationId: String.random, eventListener: nil, implementation: implementationMock, sdkInitializedUtil: sdkInitializedUtilMock) + + XCTAssertEqual(hooksMock.addCallsCount, 1) + XCTAssertEqual(hooksMock.addReceivedArguments?.key, .messagingInApp) + XCTAssertEqual(implementationMock.initializeCallsCount, 1) + XCTAssertEqual(implementationMock.initializeEventListenerCallsCount, 0) + } + + func test_initialize_givenEventListener_expectCallModuleInitializeCode() { + MessagingInApp.initialize(organizationId: String.random, eventListener: InAppEventListenerMock(), implementation: implementationMock, sdkInitializedUtil: sdkInitializedUtilMock) - XCTAssertTrue(hooksMock.addCalled) + XCTAssertEqual(hooksMock.addCallsCount, 1) XCTAssertEqual(hooksMock.addReceivedArguments?.key, .messagingInApp) + XCTAssertEqual(implementationMock.initializeCallsCount, 0) + XCTAssertEqual(implementationMock.initializeEventListenerCallsCount, 1) + } + + func test_initialize_sdkNotInitialized_expectInAppModuleNotInitialized() { + sdkInitializedUtilMock.underlyingPostInitializedData = nil // the SDK is no longer initialized + + MessagingInApp.initialize(organizationId: String.random, eventListener: nil, implementation: nil, sdkInitializedUtil: sdkInitializedUtilMock) + + XCTAssertFalse(hooksMock.addCalled) + XCTAssertFalse(hooksMock.mockCalled) + XCTAssertFalse(implementationMock.mockCalled) } }