Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 135 additions & 35 deletions UnitTests/SwiftTests/MParticle/MParticleSceneDelegateTests.swift
Original file line number Diff line number Diff line change
@@ -1,71 +1,171 @@

#if os(iOS)
#if MPARTICLE_LOCATION_DISABLE
import mParticle_Apple_SDK_NoLocation
#else
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
6 changes: 6 additions & 0 deletions mParticle-Apple-SDK.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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, ); }; };
Expand Down Expand Up @@ -932,6 +934,7 @@
7E15B2052D94617900C1FF3E /* MPRoktTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPRoktTests.m; sourceTree = "<group>"; };
7E573D292ECB65D90087185D /* MParticleSceneDelegateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MParticleSceneDelegateTests.swift; sourceTree = "<group>"; };
7E87E0BE2EE093E100A18E3A /* SceneDelegateHandlerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = SceneDelegateHandlerMock.swift; path = UnitTests/Mocks/SceneDelegateHandlerMock.swift; sourceTree = "<group>"; };
7E87E0C32EE0E84300A18E3A /* MPSceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPSceneDelegate.swift; sourceTree = "<group>"; };
B31360F72E012760000DFBC9 /* MPRoktEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MPRoktEvent.swift; sourceTree = "<group>"; };
D33C8B312B8510C20012EDFD /* MPAudience.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MPAudience.h; sourceTree = "<group>"; };
D33C8B322B8510C20012EDFD /* MPAudience.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MPAudience.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1206,6 +1209,7 @@
isa = PBXGroup;
children = (
534C11B22D08A73100466F71 /* MParticleWebView.swift */,
7E87E0C32EE0E84300A18E3A /* MPSceneDelegate.swift */,
D34286082D3806CF00FEC2C8 /* MPUserDefaults.swift */,
D3BE919A2D5B965A0038C87A /* MPLaunchInfo.swift */,
D342860E2D419CA700FEC2C8 /* MPUploadSettings.swift */,
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
36 changes: 36 additions & 0 deletions mParticle-Apple-SDK/Utils/MPSceneDelegate.swift
Original file line number Diff line number Diff line change
@@ -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<UIOpenURLContext>) {
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
Loading