@@ -67,6 +67,154 @@ final class iOSLifecycle_Tests: XCTestCase {
6767 XCTAssertTrue ( trackEvent? . event == " Application Opened " )
6868 XCTAssertTrue ( trackEvent? . type == " track " )
6969 }
70+
71+ func testApplicationForegroundedOnlyFiresAfterBackground( ) {
72+ let analytics = Analytics ( configuration: Configuration ( writeKey: " test " )
73+ . setTrackedApplicationLifecycleEvents ( . all) )
74+ let outputReader = OutputReaderPlugin ( )
75+ analytics. add ( plugin: outputReader)
76+
77+ waitUntilStarted ( analytics: analytics)
78+
79+ // Simulate: Background → Foreground
80+ NotificationCenter . default. post ( name: UIApplication . didEnterBackgroundNotification, object: nil )
81+ NotificationCenter . default. post ( name: UIApplication . willEnterForegroundNotification, object: nil )
82+
83+ let trackEvent : TrackEvent ? = outputReader. lastEvent as? TrackEvent
84+ XCTAssertEqual ( trackEvent? . event, " Application Foregrounded " ,
85+ " Application Foregrounded should fire after coming back from background " )
86+ }
87+
88+ func testTransientInterruptionDoesNotFireForegrounded( ) {
89+ let analytics = Analytics ( configuration: Configuration ( writeKey: " test " )
90+ . setTrackedApplicationLifecycleEvents ( . all) )
91+ let outputReader = OutputReaderPlugin ( )
92+ analytics. add ( plugin: outputReader)
93+
94+ waitUntilStarted ( analytics: analytics)
95+
96+ // Clear any startup events by capturing the current state
97+ let eventsBeforeInterruption = outputReader. lastEvent
98+
99+ // Simulate: willResignActive → didBecomeActive (notification center, control center, etc.)
100+ NotificationCenter . default. post ( name: UIApplication . willResignActiveNotification, object: nil )
101+ NotificationCenter . default. post ( name: UIApplication . didBecomeActiveNotification, object: nil )
102+
103+ // lastEvent should still be the same as before (no new "Application Foregrounded")
104+ let trackEvent : TrackEvent ? = outputReader. lastEvent as? TrackEvent
105+ if trackEvent? . event == " Application Foregrounded " {
106+ XCTFail ( " Application Foregrounded should NOT fire for transient interruptions like notification center " )
107+ }
108+ }
109+
110+ func testForegroundedNotFiredWithoutPriorBackground( ) {
111+ let analytics = Analytics ( configuration: Configuration ( writeKey: " test " )
112+ . setTrackedApplicationLifecycleEvents ( . all) )
113+ let outputReader = OutputReaderPlugin ( )
114+ analytics. add ( plugin: outputReader)
115+
116+ waitUntilStarted ( analytics: analytics)
117+
118+ // Simulate: willEnterForeground without prior didEnterBackground
119+ NotificationCenter . default. post ( name: UIApplication . willEnterForegroundNotification, object: nil )
120+
121+ let trackEvent : TrackEvent ? = outputReader. lastEvent as? TrackEvent
122+ XCTAssertNotEqual ( trackEvent? . event, " Application Foregrounded " ,
123+ " Application Foregrounded should not fire without a prior background event " )
124+ }
125+
126+ func testMultipleBackgroundForegroundCycles( ) {
127+ let analytics = Analytics ( configuration: Configuration ( writeKey: " test " )
128+ . setTrackedApplicationLifecycleEvents ( . all) )
129+ let outputReader = OutputReaderPlugin ( )
130+ analytics. add ( plugin: outputReader)
131+
132+ waitUntilStarted ( analytics: analytics)
133+
134+ // Cycle 1: Background → Foreground
135+ NotificationCenter . default. post ( name: UIApplication . didEnterBackgroundNotification, object: nil )
136+ NotificationCenter . default. post ( name: UIApplication . willEnterForegroundNotification, object: nil )
137+
138+ var trackEvent : TrackEvent ? = outputReader. lastEvent as? TrackEvent
139+ XCTAssertEqual ( trackEvent? . event, " Application Foregrounded " ,
140+ " First foreground cycle should fire Application Foregrounded " )
141+
142+ // Cycle 2: Background → Foreground
143+ NotificationCenter . default. post ( name: UIApplication . didEnterBackgroundNotification, object: nil )
144+ NotificationCenter . default. post ( name: UIApplication . willEnterForegroundNotification, object: nil )
145+
146+ trackEvent = outputReader. lastEvent as? TrackEvent
147+ XCTAssertEqual ( trackEvent? . event, " Application Foregrounded " ,
148+ " Second foreground cycle should also fire Application Foregrounded " )
149+ }
150+
151+ func testBackgroundAlwaysFires( ) {
152+ let analytics = Analytics ( configuration: Configuration ( writeKey: " test " )
153+ . setTrackedApplicationLifecycleEvents ( . all) )
154+ let outputReader = OutputReaderPlugin ( )
155+ analytics. add ( plugin: outputReader)
156+
157+ waitUntilStarted ( analytics: analytics)
158+
159+ // Simulate: Background
160+ NotificationCenter . default. post ( name: UIApplication . didEnterBackgroundNotification, object: nil )
161+
162+ let trackEvent : TrackEvent ? = outputReader. lastEvent as? TrackEvent
163+ XCTAssertEqual ( trackEvent? . event, " Application Backgrounded " ,
164+ " Application Backgrounded should always fire when app enters background " )
165+ }
166+
167+ func testComplexLifecycleSequence( ) {
168+ let analytics = Analytics ( configuration: Configuration ( writeKey: " test " )
169+ . setTrackedApplicationLifecycleEvents ( . all) )
170+ let outputReader = OutputReaderPlugin ( )
171+ analytics. add ( plugin: outputReader)
172+
173+ waitUntilStarted ( analytics: analytics)
174+
175+ // Simulate realistic user behavior:
176+ // 1. Background the app
177+ NotificationCenter . default. post ( name: UIApplication . didEnterBackgroundNotification, object: nil )
178+ var trackEvent : TrackEvent ? = outputReader. lastEvent as? TrackEvent
179+ XCTAssertEqual ( trackEvent? . event, " Application Backgrounded " )
180+
181+ // 2. Foreground the app
182+ NotificationCenter . default. post ( name: UIApplication . willEnterForegroundNotification, object: nil )
183+ trackEvent = outputReader. lastEvent as? TrackEvent
184+ XCTAssertEqual ( trackEvent? . event, " Application Foregrounded " )
185+
186+ // 3. Pull down notification center (transient interruption)
187+ NotificationCenter . default. post ( name: UIApplication . willResignActiveNotification, object: nil )
188+ NotificationCenter . default. post ( name: UIApplication . didBecomeActiveNotification, object: nil )
189+
190+ // Last event should still be "Application Foregrounded" from step 2
191+ trackEvent = outputReader. lastEvent as? TrackEvent
192+ XCTAssertEqual ( trackEvent? . event, " Application Foregrounded " ,
193+ " Transient interruption should not create new events " )
194+
195+ // 4. Background again
196+ NotificationCenter . default. post ( name: UIApplication . didEnterBackgroundNotification, object: nil )
197+ trackEvent = outputReader. lastEvent as? TrackEvent
198+ XCTAssertEqual ( trackEvent? . event, " Application Backgrounded " )
199+ }
200+
201+ func testDidBecomeActiveDoesNotFireForegrounded( ) {
202+ let analytics = Analytics ( configuration: Configuration ( writeKey: " test " )
203+ . setTrackedApplicationLifecycleEvents ( . all) )
204+ let outputReader = OutputReaderPlugin ( )
205+ analytics. add ( plugin: outputReader)
206+
207+ waitUntilStarted ( analytics: analytics)
208+
209+ // Simulate: didBecomeActive (should not fire anything anymore)
210+ NotificationCenter . default. post ( name: UIApplication . didBecomeActiveNotification, object: nil )
211+
212+ // Verify no new event was created
213+ let trackEvent : TrackEvent ? = outputReader. lastEvent as? TrackEvent
214+ if trackEvent? . event == " Application Foregrounded " {
215+ XCTFail ( " didBecomeActive should not fire Application Foregrounded anymore " )
216+ }
217+ }
70218}
71219
72220#endif
0 commit comments