66#import < XCTest/XCTest.h>
77#import " ScreenBeforeFlutter.h"
88
9+ @interface XCAppLifecycleTestExpectation : XCTestExpectation
10+
11+ - (instancetype )initForLifecycle : (NSString *)expectedLifecycle forStep : (NSString *)step ;
12+ @property (nonatomic , readonly ) NSString * expectedLifecycle;
13+
14+ @end
15+
16+ @implementation XCAppLifecycleTestExpectation
17+
18+ @synthesize expectedLifecycle = _expectedLifecycle;
19+ - (instancetype )initForLifecycle : (NSString *)expectedLifecycle forStep : (NSString *)step {
20+ // The step is here because the callbacks into the handler which checks these expectations isn't
21+ // synchronous with the executions in the test, so it's hard to find the cause in the test
22+ // otherwise.
23+ self = [self initWithDescription: [NSString stringWithFormat: @" Expected state %@ during step %@ " ,
24+ expectedLifecycle, step]];
25+ _expectedLifecycle = expectedLifecycle;
26+ return self;
27+ }
28+
29+ @end
30+
931@interface AppLifecycleTests : XCTestCase
1032@end
1133
@@ -32,59 +54,56 @@ - (void)testDismissedFlutterViewControllerNotRespondingToApplicationLifecycle {
3254 FlutterEngine* engine = rootVC.engine ;
3355
3456 NSMutableArray * lifecycleExpectations = [NSMutableArray arrayWithCapacity: 10 ];
35- NSMutableArray * lifecycleEvents = [NSMutableArray arrayWithCapacity: 10 ];
3657
37- [lifecycleExpectations addObject: [[XCTestExpectation alloc ]
38- initWithDescription: @" A loading FlutterViewController goes "
39- @" through AppLifecycleState.inactive" ]];
40- [lifecycleExpectations
41- addObject: [[XCTestExpectation alloc ]
42- initWithDescription:
43- @" A loading FlutterViewController goes through AppLifecycleState.resumed " ]];
58+ // Expected sequence from showing the FlutterViewController is inactive and resumed.
59+ [lifecycleExpectations addObjectsFromArray: @[
60+ [[XCAppLifecycleTestExpectation alloc ] initForLifecycle: @" AppLifecycleState.inactive"
61+ forStep: @" showing a FlutterViewController " ],
62+ [[XCAppLifecycleTestExpectation alloc ] initForLifecycle: @" AppLifecycleState.resumed "
63+ forStep: @" showing a FlutterViewController " ]
64+ ]];
4465
4566 // Holding onto this FlutterViewController is consequential here. Since a released
4667 // FlutterViewController wouldn't keep listening to the application lifecycle events and produce
47- // false positives.
68+ // false positives for the application lifecycle tests further below .
4869 FlutterViewController* flutterVC = [rootVC showFlutter ];
4970 [engine.lifecycleChannel setMessageHandler: ^(id message, FlutterReply callback) {
5071 if (lifecycleExpectations.count == 0 ) {
5172 XCTFail (@" Unexpected lifecycle transition: %@ " , message);
73+ return ;
5274 }
53- [lifecycleEvents addObject: message];
54- [[lifecycleExpectations objectAtIndex: 0 ] fulfill ];
75+ XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex: 0 ];
76+ if (![[nextExpectation expectedLifecycle ] isEqualToString: message]) {
77+ XCTFail (@" Expected lifecycle %@ but instead received %@ " , [nextExpectation expectedLifecycle ],
78+ message);
79+ return ;
80+ }
81+
82+ [nextExpectation fulfill ];
5583 [lifecycleExpectations removeObjectAtIndex: 0 ];
5684 }];
5785
58- [self waitForExpectations: lifecycleExpectations timeout: 5 ];
59-
60- // Expected sequence from showing the FlutterViewController is inactive and resumed.
61- NSArray * expectedStates = @[ @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" ];
62- XCTAssertEqualObjects (lifecycleEvents, expectedStates,
63- @" AppLifecycleState transitions while presenting not as expected" );
86+ // The expectations list isn't dequeued by the message handler yet.
87+ [self waitForExpectations: lifecycleExpectations timeout: 5 enforceOrder: YES ];
6488
6589 // Now dismiss the FlutterViewController again and expect another inactive and paused.
66- [lifecycleExpectations
67- addObject: [[XCTestExpectation alloc ]
68- initWithDescription: @" A dismissed FlutterViewController goes through "
69- @" AppLifecycleState.inactive" ]];
70- [lifecycleExpectations
71- addObject: [[XCTestExpectation alloc ]
72- initWithDescription: @" A dismissed FlutterViewController goes through "
73- @" AppLifecycleState.paused" ]];
90+ [lifecycleExpectations addObjectsFromArray: @[
91+ [[XCAppLifecycleTestExpectation alloc ] initForLifecycle: @" AppLifecycleState.inactive"
92+ forStep: @" dismissing a FlutterViewController" ],
93+ [[XCAppLifecycleTestExpectation alloc ]
94+ initForLifecycle: @" AppLifecycleState.paused"
95+ forStep: @" dismissing a FlutterViewController" ]
96+ ]];
7497 [flutterVC dismissViewControllerAnimated: NO completion: nil ];
75- [self waitForExpectations: lifecycleExpectations timeout: 5 ];
76- expectedStates = @[
77- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" , @" AppLifecycleState.inactive" ,
78- @" AppLifecycleState.paused"
79- ];
80- XCTAssertEqualObjects (lifecycleEvents, expectedStates,
81- @" AppLifecycleState transitions while dismissing not as expected" );
98+ [self waitForExpectations: lifecycleExpectations timeout: 5 enforceOrder: YES ];
8299
83100 // Now put the app in the background (while the engine is still running) and bring it back to
84101 // the foreground. Granted, we're not winning any awards for hyper-realism but at least we're
85102 // checking that we aren't observing the UIApplication notifications and double registering
86103 // for AppLifecycleState events.
87104
105+ // These operations are synchronous so if they trigger any lifecycle events, they should trigger
106+ // failures in the message handler immediately.
88107 [[NSNotificationCenter defaultCenter ]
89108 postNotificationName: UIApplicationWillResignActiveNotification
90109 object: nil ];
@@ -100,26 +119,19 @@ - (void)testDismissedFlutterViewControllerNotRespondingToApplicationLifecycle {
100119
101120 // There's no timing latch for our semi-fake background-foreground cycle so launch the
102121 // FlutterViewController again to check the complete event list again.
103- [lifecycleExpectations addObject: [[XCTestExpectation alloc ]
104- initWithDescription: @" A second FlutterViewController goes "
105- @" through AppLifecycleState.inactive" ]];
106- [lifecycleExpectations
107- addObject: [[XCTestExpectation alloc ]
108- initWithDescription:
109- @" A second FlutterViewController goes through AppLifecycleState.resumed" ]];
122+
123+ // Expect only lifecycle events from showing the FlutterViewController again, not from any
124+ // backgrounding/foregrounding.
125+ [lifecycleExpectations addObjectsFromArray: @[
126+ [[XCAppLifecycleTestExpectation alloc ]
127+ initForLifecycle: @" AppLifecycleState.inactive"
128+ forStep: @" showing a FlutterViewController a second time after backgrounding" ],
129+ [[XCAppLifecycleTestExpectation alloc ]
130+ initForLifecycle: @" AppLifecycleState.resumed"
131+ forStep: @" showing a FlutterViewController a second time after backgrounding" ]
132+ ]];
110133 flutterVC = [rootVC showFlutter ];
111- [self waitForExpectations: lifecycleExpectations timeout: 5 ];
112- expectedStates = @[
113- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" , @" AppLifecycleState.inactive" ,
114- @" AppLifecycleState.paused" ,
115-
116- // We only added 2 from re-launching the FlutterViewController
117- // and none from the background-foreground cycle.
118- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed"
119- ];
120- XCTAssertEqualObjects (
121- lifecycleEvents, expectedStates,
122- @" AppLifecycleState transitions while presenting a second time not as expected" );
134+ [self waitForExpectations: lifecycleExpectations timeout: 5 enforceOrder: YES ];
123135
124136 // Dismantle.
125137 [engine.lifecycleChannel setMessageHandler: nil ];
@@ -146,78 +158,67 @@ - (void)testVisibleFlutterViewControllerRespondsToApplicationLifecycle {
146158 FlutterEngine* engine = rootVC.engine ;
147159
148160 NSMutableArray * lifecycleExpectations = [NSMutableArray arrayWithCapacity: 10 ];
149- NSMutableArray * lifecycleEvents = [NSMutableArray arrayWithCapacity: 10 ];
150161
151- [lifecycleExpectations addObject: [[XCTestExpectation alloc ]
152- initWithDescription: @" A loading FlutterViewController goes "
153- @" through AppLifecycleState.inactive" ]];
154- [lifecycleExpectations
155- addObject: [[XCTestExpectation alloc ]
156- initWithDescription:
157- @" A loading FlutterViewController goes through AppLifecycleState.resumed " ]];
162+ // Expected sequence from showing the FlutterViewController is inactive and resumed.
163+ [lifecycleExpectations addObjectsFromArray: @[
164+ [[XCAppLifecycleTestExpectation alloc ] initForLifecycle: @" AppLifecycleState.inactive"
165+ forStep: @" showing a FlutterViewController " ],
166+ [[XCAppLifecycleTestExpectation alloc ] initForLifecycle: @" AppLifecycleState.resumed "
167+ forStep: @" showing a FlutterViewController " ]
168+ ]];
158169
159170 FlutterViewController* flutterVC = [rootVC showFlutter ];
160171 [engine.lifecycleChannel setMessageHandler: ^(id message, FlutterReply callback) {
161172 if (lifecycleExpectations.count == 0 ) {
162173 XCTFail (@" Unexpected lifecycle transition: %@ " , message);
174+ return ;
175+ }
176+ XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex: 0 ];
177+ if (![[nextExpectation expectedLifecycle ] isEqualToString: message]) {
178+ XCTFail (@" Expected lifecycle %@ but instead received %@ " , [nextExpectation expectedLifecycle ],
179+ message);
180+ return ;
163181 }
164- [lifecycleEvents addObject: message];
165- [[lifecycleExpectations objectAtIndex: 0 ] fulfill ];
182+
183+ [nextExpectation fulfill ];
166184 [lifecycleExpectations removeObjectAtIndex: 0 ];
167185 }];
168186
169187 [self waitForExpectations: lifecycleExpectations timeout: 5 ];
170188
171- // Expected sequence from showing the FlutterViewController is inactive and resumed.
172- NSArray * expectedStates = @[ @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" ];
173- XCTAssertEqualObjects (lifecycleEvents, expectedStates,
174- @" AppLifecycleState transitions while presenting not as expected" );
175-
176189 // Now put the FlutterViewController into background.
177- [lifecycleExpectations
178- addObject: [[XCTestExpectation alloc ]
179- initWithDescription :@" A backgrounding FlutterViewController goes through "
180- @" AppLifecycleState.inactive " ]];
181- [lifecycleExpectations
182- addObject: [[XCTestExpectation alloc ]
183- initWithDescription :@" A backgrounding FlutterViewController goes through "
184- @" AppLifecycleState.paused " ]];
190+ [lifecycleExpectations addObjectsFromArray: @[
191+ [[XCAppLifecycleTestExpectation alloc ]
192+ initForLifecycle :@" AppLifecycleState.inactive "
193+ forStep: @" putting FlutterViewController to the background " ],
194+ [[XCAppLifecycleTestExpectation alloc ]
195+ initForLifecycle: @" AppLifecycleState.paused "
196+ forStep :@" putting FlutterViewController to the background " ]
197+ ]];
185198 [[NSNotificationCenter defaultCenter ]
186199 postNotificationName: UIApplicationWillResignActiveNotification
187200 object: nil ];
188201 [[NSNotificationCenter defaultCenter ]
189202 postNotificationName: UIApplicationDidEnterBackgroundNotification
190203 object: nil ];
191204 [self waitForExpectations: lifecycleExpectations timeout: 5 ];
192- expectedStates = @[
193- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" , @" AppLifecycleState.inactive" ,
194- @" AppLifecycleState.paused"
195- ];
196- XCTAssertEqualObjects (lifecycleEvents, expectedStates,
197- @" AppLifecycleState transitions while backgrounding not as expected" );
198205
199206 // Now restore to foreground
200- [lifecycleExpectations
201- addObject: [[XCTestExpectation alloc ]
202- initWithDescription :@" A foregrounding FlutterViewController goes through "
203- @" AppLifecycleState.inactive " ]];
204- [lifecycleExpectations
205- addObject: [[XCTestExpectation alloc ]
206- initWithDescription :@" A foregrounding FlutterViewController goes through "
207- @" AppLifecycleState.paused " ]];
207+ [lifecycleExpectations addObjectsFromArray: @[
208+ [[XCAppLifecycleTestExpectation alloc ]
209+ initForLifecycle :@" AppLifecycleState.inactive "
210+ forStep: @" putting FlutterViewController back to foreground " ],
211+ [[XCAppLifecycleTestExpectation alloc ]
212+ initForLifecycle: @" AppLifecycleState.resumed "
213+ forStep :@" putting FlutterViewController back to foreground " ]
214+ ]];
208215 [[NSNotificationCenter defaultCenter ]
209216 postNotificationName: UIApplicationWillEnterForegroundNotification
210217 object: nil ];
211218 [[NSNotificationCenter defaultCenter ]
212219 postNotificationName: UIApplicationDidBecomeActiveNotification
213220 object: nil ];
214221 [self waitForExpectations: lifecycleExpectations timeout: 5 ];
215- expectedStates = @[
216- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" , @" AppLifecycleState.inactive" ,
217- @" AppLifecycleState.paused" , @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed"
218- ];
219- XCTAssertEqualObjects (lifecycleEvents, expectedStates,
220- @" AppLifecycleState transitions while foregrounding not as expected" );
221222
222223 // Dismantle.
223224 [engine.lifecycleChannel setMessageHandler: nil ];
0 commit comments