diff --git a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift index 002a41967..d9ca616ec 100644 --- a/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift +++ b/UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift @@ -1,4 +1,4 @@ - +#if os(iOS) #if MPARTICLE_LOCATION_DISABLE import mParticle_Apple_SDK_NoLocation #else @@ -6,66 +6,166 @@ import mParticle_Apple_SDK #endif import XCTest -final class MParticleSceneDelegateTests: XCTestCase { +// MARK: - SceneDelegateHandler Tests + +final class SceneDelegateHandlerTests: XCTestCase { + + // MARK: - Properties + var handler: SceneDelegateHandler! + var mockOpenURLHandler: OpenURLHandlerProtocolMock! + var logger: MPLog! + + // MARK: - Setup/Teardown + + override func setUp() { + super.setUp() + logger = MPLog(logLevel: .verbose) + mockOpenURLHandler = OpenURLHandlerProtocolMock() + handler = SceneDelegateHandler(logger: logger, appNotificationHandler: mockOpenURLHandler) + } + + override func tearDown() { + handler = nil + mockOpenURLHandler = nil + logger = nil + super.tearDown() + } + + // MARK: - Initialization Tests + + func test_init_createsHandler() { + XCTAssertNotNil(handler) + } + + // MARK: - handleUserActivity Tests + + func test_handleUserActivity_callsAppNotificationHandler() { + // Arrange + let userActivity = NSUserActivity(activityType: "com.test.activity") + userActivity.title = "Test Activity" + + // Act + handler.handleUserActivity(userActivity) + + // Assert + XCTAssertTrue(mockOpenURLHandler.continueUserActivityCalled) + XCTAssertEqual(mockOpenURLHandler.continueUserActivityUserActivityParam, userActivity) + } + + func test_handleUserActivity_withWebBrowsingActivity_callsHandler() { + // Arrange + let webActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) + webActivity.webpageURL = URL(string: "https://example.com/deep/link") + + // Act + handler.handleUserActivity(webActivity) + + // Assert + XCTAssertTrue(mockOpenURLHandler.continueUserActivityCalled) + XCTAssertEqual(mockOpenURLHandler.continueUserActivityUserActivityParam?.activityType, NSUserActivityTypeBrowsingWeb) + XCTAssertEqual(mockOpenURLHandler.continueUserActivityUserActivityParam?.webpageURL?.absoluteString, "https://example.com/deep/link") + } + + func test_handleUserActivity_restorationHandlerIsSafeToCall() { + // Arrange + let userActivity = NSUserActivity(activityType: "com.test.activity") + + // Act + handler.handleUserActivity(userActivity) + + // Assert - calling the restoration handler should not crash + let restorationHandler = mockOpenURLHandler.continueUserActivityRestorationHandlerParam + XCTAssertNotNil(restorationHandler) + XCTAssertNoThrow(restorationHandler?(nil)) + XCTAssertNoThrow(restorationHandler?([])) + } +} + +// MARK: - MParticle Scene Delegate Integration Tests + +final class MParticleSceneDelegateIntegrationTests: XCTestCase { // MARK: - Properties var mparticle: MParticle! var sceneMock: OpenURLHandlerProtocolMock! - var testURL: URL! var testUserActivity: NSUserActivity! override func setUp() { super.setUp() mparticle = MParticle() - testURL = URL(string: "myapp://test/path?param=value")! testUserActivity = NSUserActivity(activityType: "com.test.activity") testUserActivity.title = "Test Activity" testUserActivity.userInfo = ["key": "value"] - // The implementation calls [MParticle sharedInstance], so we need to set the mock on the shared instance + // Set up mock on the scene delegate handler sceneMock = OpenURLHandlerProtocolMock() let sceneHandler = SceneDelegateHandler(logger: MPLog(logLevel: .verbose), appNotificationHandler: sceneMock) mparticle.sceneDelegateHandler = sceneHandler } - - // MARK: - handleUserActivity Tests - func test_handleUserActivity_invokesAppNotificationHandler() { + + override func tearDown() { + mparticle = nil + sceneMock = nil + testUserActivity = nil + super.tearDown() + } + + // MARK: - handleUserActivity via MParticle Tests + + func test_handleUserActivity_invokesSceneDelegateHandler() { // Act mparticle.handleUserActivity(testUserActivity) - // Assert - handleUserActivity directly calls the app notification handler + // Assert XCTAssertTrue(sceneMock.continueUserActivityCalled) XCTAssertEqual(sceneMock.continueUserActivityUserActivityParam, testUserActivity) XCTAssertNotNil(sceneMock.continueUserActivityRestorationHandlerParam) } +} + +// MARK: - MPSceneDelegate Tests + +@available(iOS 13.0, *) +final class MPSceneDelegateTests: XCTestCase { - func test_handleUserActivity_withWebBrowsingActivity() { - // Arrange - let webActivity = NSUserActivity(activityType: NSUserActivityTypeBrowsingWeb) - webActivity.title = "Web Page" - webActivity.webpageURL = URL(string: "https://example.com/page") - - // Act - mparticle.handleUserActivity(webActivity) - - // Assert - Direct call to app notification handler - XCTAssertTrue(sceneMock.continueUserActivityCalled) - XCTAssertEqual(sceneMock.continueUserActivityUserActivityParam, webActivity) + var sceneDelegate: MPSceneDelegate! + + override func setUp() { + super.setUp() + sceneDelegate = MPSceneDelegate() } - func test_handleUserActivity_restorationHandlerIsEmpty() { - // Act - mparticle.handleUserActivity(testUserActivity) - - // Assert - XCTAssertTrue(sceneMock.continueUserActivityCalled) - - // Verify the restoration handler is provided and safe to call - let restorationHandler = sceneMock.continueUserActivityRestorationHandlerParam - XCTAssertNotNil(restorationHandler) - - // Test that calling the restoration handler doesn't crash - XCTAssertNoThrow(restorationHandler?(nil)) - XCTAssertNoThrow(restorationHandler?([])) + override func tearDown() { + sceneDelegate = nil + super.tearDown() + } + + // MARK: - Initialization Tests + + func test_init_createsInstance() { + XCTAssertNotNil(sceneDelegate) + } + + func test_conformsToUIWindowSceneDelegate() { + XCTAssertTrue(sceneDelegate is UIWindowSceneDelegate) + } + + // MARK: - Method Existence Tests + // Verify the key delegate methods exist (actual functionality tested via integration tests) + + func test_respondsToSceneWillConnectTo() { + let selector = #selector(UIWindowSceneDelegate.scene(_:willConnectTo:options:)) + XCTAssertTrue(sceneDelegate.responds(to: selector)) + } + + func test_respondsToSceneOpenURLContexts() { + let selector = #selector(UIWindowSceneDelegate.scene(_:openURLContexts:)) + XCTAssertTrue(sceneDelegate.responds(to: selector)) + } + + func test_respondsToSceneContinueUserActivity() { + let selector = #selector(UIWindowSceneDelegate.scene(_:continue:)) + XCTAssertTrue(sceneDelegate.responds(to: selector)) } } +#endif diff --git a/mParticle-Apple-SDK.xcodeproj/project.pbxproj b/mParticle-Apple-SDK.xcodeproj/project.pbxproj index 747d53a29..253c20e92 100644 --- a/mParticle-Apple-SDK.xcodeproj/project.pbxproj +++ b/mParticle-Apple-SDK.xcodeproj/project.pbxproj @@ -572,6 +572,8 @@ 7E573D2B2ECB65D90087185D /* MParticleSceneDelegateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */; }; 7E87E0C12EE093E100A18E3A /* SceneDelegateHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */; }; 7E87E0C22EE093E100A18E3A /* SceneDelegateHandlerMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */; }; + 7E87E0C42EE0E84F00A18E3A /* MPSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E87E0C32EE0E84300A18E3A /* MPSceneDelegate.swift */; }; + 7E87E0C52EE0E84F00A18E3A /* MPSceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7E87E0C32EE0E84300A18E3A /* MPSceneDelegate.swift */; }; B31360F92E012760000DFBC9 /* MPRoktEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31360F72E012760000DFBC9 /* MPRoktEvent.swift */; }; B3D778622E02F55F00D887A4 /* MPRoktEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = B31360F72E012760000DFBC9 /* MPRoktEvent.swift */; }; D30CD0CB2CFF5FB100F5148A /* MPStateMachine.h in Headers */ = {isa = PBXBuildFile; fileRef = 53A79B0629CDFB1F00E7489F /* MPStateMachine.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -932,6 +934,7 @@ 7E15B2052D94617900C1FF3E /* MPRoktTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPRoktTests.m; sourceTree = ""; }; 7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MParticleSceneDelegateTests.swift; sourceTree = ""; }; 7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegateHandlerMock.swift; path = UnitTests/Mocks/SceneDelegateHandlerMock.swift; sourceTree = ""; }; + 7E87E0C32EE0E84300A18E3A /* MPSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSceneDelegate.swift; sourceTree = ""; }; B31360F72E012760000DFBC9 /* MPRoktEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPRoktEvent.swift; sourceTree = ""; }; D33C8B312B8510C20012EDFD /* MPAudience.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPAudience.h; sourceTree = ""; }; D33C8B322B8510C20012EDFD /* MPAudience.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPAudience.m; sourceTree = ""; }; @@ -1206,6 +1209,7 @@ isa = PBXGroup; children = ( 534C11B22D08A73100466F71 /* MParticleWebView.swift */, + 7E87E0C32EE0E84300A18E3A /* MPSceneDelegate.swift */, D34286082D3806CF00FEC2C8 /* MPUserDefaults.swift */, D3BE919A2D5B965A0038C87A /* MPLaunchInfo.swift */, D342860E2D419CA700FEC2C8 /* MPUploadSettings.swift */, @@ -2105,6 +2109,7 @@ 53A79B6729CDFB2000E7489F /* MPAliasResponse.m in Sources */, 7E0387842DB913D2003B7D5E /* MPRokt.m in Sources */, 35E3FCC12E53B55900DB5B18 /* MPAttributionResult+MParticlePrivate.m in Sources */, + 7E87E0C42EE0E84F00A18E3A /* MPSceneDelegate.swift in Sources */, 534C11B72D08F53D00466F71 /* MPUserAttributeChange.swift in Sources */, D342860F2D419CB800FEC2C8 /* MPUploadSettings.swift in Sources */, 53A79C1229CDFB2100E7489F /* MPKitFilter.m in Sources */, @@ -2327,6 +2332,7 @@ 53A79DAB29CE23F700E7489F /* MPEvent.m in Sources */, 7E0387822DB913D2003B7D5E /* MPRokt.m in Sources */, 35E3FCC02E53B55900DB5B18 /* MPAttributionResult+MParticlePrivate.m in Sources */, + 7E87E0C52EE0E84F00A18E3A /* MPSceneDelegate.swift in Sources */, 534C11B62D08F53D00466F71 /* MPUserAttributeChange.swift in Sources */, D34286102D419CB800FEC2C8 /* MPUploadSettings.swift in Sources */, 53A79DAD29CE23F700E7489F /* MPAliasResponse.m in Sources */, diff --git a/mParticle-Apple-SDK/Utils/MPSceneDelegate.swift b/mParticle-Apple-SDK/Utils/MPSceneDelegate.swift new file mode 100644 index 000000000..d14fbc315 --- /dev/null +++ b/mParticle-Apple-SDK/Utils/MPSceneDelegate.swift @@ -0,0 +1,36 @@ +#if os(iOS) +import UIKit + +@available(iOS 13.0, *) +public class MPSceneDelegate: NSObject, UIWindowSceneDelegate { + + public var window: UIWindow? + + // Called when scene connects + public func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + guard scene is UIWindowScene else { return } + + // Handle URLs passed during launch + if let urlContext = connectionOptions.urlContexts.first { + MParticle.sharedInstance().handleURLContext(urlContext) + } + + // Handle user activities (Universal Links) + if let userActivity = connectionOptions.userActivities.first { + MParticle.sharedInstance().handleUserActivity(userActivity) + } + } + + // Called when app is already running and receives URL + public func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + for context in URLContexts { + MParticle.sharedInstance().handleURLContext(context) + } + } + + // Called for Universal Links + public func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + MParticle.sharedInstance().handleUserActivity(userActivity) + } +} +#endif