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 , copy ) 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 = [super initWithDescription: [NSString stringWithFormat: @" Expected state %@ during step %@ " ,
24+ expectedLifecycle, step]];
25+ _expectedLifecycle = [expectedLifecycle copy ];
26+ return self;
27+ }
28+
29+ @end
30+
931@interface AppLifecycleTests : XCTestCase
1032@end
1133
@@ -16,7 +38,7 @@ - (void)setUp {
1638 self.continueAfterFailure = NO ;
1739}
1840
19- - (void )testLifecycleChannel {
41+ - (void )testDismissedFlutterViewControllerNotRespondingToApplicationLifecycle {
2042 XCTestExpectation* engineStartedExpectation = [self expectationWithDescription: @" Engine started" ];
2143
2244 // Let the engine finish booting (at the end of which the channels are properly set-up) before
@@ -32,121 +54,173 @@ - (void)testLifecycleChannel {
3254 FlutterEngine* engine = rootVC.engine ;
3355
3456 NSMutableArray * lifecycleExpectations = [NSMutableArray arrayWithCapacity: 10 ];
35- NSMutableArray * lifecycleEvents = [NSMutableArray arrayWithCapacity: 10 ];
36-
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" ]];
4457
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+ ]];
65+
66+ // Holding onto this FlutterViewController is consequential here. Since a released
67+ // FlutterViewController wouldn't keep listening to the application lifecycle events and produce
68+ // false positives for the application lifecycle tests further below.
4569 FlutterViewController* flutterVC = [rootVC showFlutter ];
4670 [engine.lifecycleChannel setMessageHandler: ^(id message, FlutterReply callback) {
4771 if (lifecycleExpectations.count == 0 ) {
4872 XCTFail (@" Unexpected lifecycle transition: %@ " , message);
73+ return ;
74+ }
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 ;
4980 }
50- [lifecycleEvents addObject: message];
51- [[lifecycleExpectations objectAtIndex: 0 ] fulfill ];
81+
82+ [nextExpectation fulfill ];
5283 [lifecycleExpectations removeObjectAtIndex: 0 ];
5384 }];
5485
55- [self waitForExpectations: lifecycleExpectations timeout: 5 ];
56-
57- // Expected sequence from showing the FlutterViewController is inactive and resumed.
58- NSArray * expectedStates = @[ @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" ];
59- XCTAssertEqualObjects (lifecycleEvents, expectedStates,
60- @" 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 ];
6188
6289 // Now dismiss the FlutterViewController again and expect another inactive and paused.
63- [lifecycleExpectations
64- addObject: [[XCTestExpectation alloc ]
65- initWithDescription: @" A dismissed FlutterViewController goes through "
66- @" AppLifecycleState.inactive" ]];
67- [lifecycleExpectations
68- addObject: [[XCTestExpectation alloc ]
69- initWithDescription: @" A dismissed FlutterViewController goes through "
70- @" 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+ ]];
7197 [flutterVC dismissViewControllerAnimated: NO completion: nil ];
72- [self waitForExpectations: lifecycleExpectations timeout: 5 ];
73- expectedStates = @[
74- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" , @" AppLifecycleState.inactive" ,
75- @" AppLifecycleState.paused"
76- ];
77- XCTAssertEqualObjects (lifecycleEvents, expectedStates,
78- @" AppLifecycleState transitions while dismissing not as expected" );
98+ [self waitForExpectations: lifecycleExpectations timeout: 5 enforceOrder: YES ];
7999
80100 // Now put the app in the background (while the engine is still running) and bring it back to
81101 // the foreground. Granted, we're not winning any awards for hyper-realism but at least we're
82102 // checking that we aren't observing the UIApplication notifications and double registering
83103 // for AppLifecycleState events.
84104
85- // However the production is temporarily wrong. https://github.com/flutter/flutter/issues/37226.
86- // It will be fixed in a next PR that removes the wrong asserts.
87- [lifecycleExpectations
88- addObject:
89- [[XCTestExpectation alloc ]
90- initWithDescription: @" Current implementation sends another AppLifecycleState event" ]];
105+ // These operations are synchronous so if they trigger any lifecycle events, they should trigger
106+ // failures in the message handler immediately.
91107 [[NSNotificationCenter defaultCenter ]
92108 postNotificationName: UIApplicationWillResignActiveNotification
93109 object: nil ];
94- [lifecycleExpectations
95- addObject:
96- [[XCTestExpectation alloc ]
97- initWithDescription: @" Current implementation sends another AppLifecycleState event" ]];
98110 [[NSNotificationCenter defaultCenter ]
99111 postNotificationName: UIApplicationDidEnterBackgroundNotification
100112 object: nil ];
101- [lifecycleExpectations
102- addObject:
103- [[XCTestExpectation alloc ]
104- initWithDescription: @" Current implementation sends another AppLifecycleState event" ]];
105113 [[NSNotificationCenter defaultCenter ]
106114 postNotificationName: UIApplicationWillEnterForegroundNotification
107115 object: nil ];
108- [lifecycleExpectations
109- addObject:
110- [[XCTestExpectation alloc ]
111- initWithDescription: @" Current implementation sends another AppLifecycleState event" ]];
112116 [[NSNotificationCenter defaultCenter ]
113117 postNotificationName: UIApplicationDidBecomeActiveNotification
114118 object: nil ];
115119
116120 // There's no timing latch for our semi-fake background-foreground cycle so launch the
117121 // FlutterViewController again to check the complete event list again.
118- [lifecycleExpectations addObject: [[XCTestExpectation alloc ]
119- initWithDescription: @" A second FlutterViewController goes "
120- @" through AppLifecycleState.inactive" ]];
121- [lifecycleExpectations
122- addObject: [[XCTestExpectation alloc ]
123- initWithDescription:
124- @" 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+ ]];
125133 flutterVC = [rootVC showFlutter ];
134+ [self waitForExpectations: lifecycleExpectations timeout: 5 enforceOrder: YES ];
135+
136+ // Dismantle.
137+ [engine.lifecycleChannel setMessageHandler: nil ];
138+ [flutterVC dismissViewControllerAnimated: NO completion: nil ];
139+ [engine setViewController: nil ];
140+ }
141+
142+ - (void )testVisibleFlutterViewControllerRespondsToApplicationLifecycle {
143+ XCTestExpectation* engineStartedExpectation = [self expectationWithDescription: @" Engine started" ];
144+
145+ // Let the engine finish booting (at the end of which the channels are properly set-up) before
146+ // moving onto the next step of showing the next view controller.
147+ ScreenBeforeFlutter* rootVC = [[ScreenBeforeFlutter alloc ] initWithEngineRunCompletion: ^void () {
148+ [engineStartedExpectation fulfill ];
149+ }];
150+
151+ [self waitForExpectationsWithTimeout: 5 handler: nil ];
152+
153+ UIApplication* application = UIApplication.sharedApplication ;
154+ application.delegate .window .rootViewController = rootVC;
155+ FlutterEngine* engine = rootVC.engine ;
156+
157+ NSMutableArray * lifecycleExpectations = [NSMutableArray arrayWithCapacity: 10 ];
158+
159+ // Expected sequence from showing the FlutterViewController is inactive and resumed.
160+ [lifecycleExpectations addObjectsFromArray: @[
161+ [[XCAppLifecycleTestExpectation alloc ] initForLifecycle: @" AppLifecycleState.inactive"
162+ forStep: @" showing a FlutterViewController" ],
163+ [[XCAppLifecycleTestExpectation alloc ] initForLifecycle: @" AppLifecycleState.resumed"
164+ forStep: @" showing a FlutterViewController" ]
165+ ]];
166+
167+ FlutterViewController* flutterVC = [rootVC showFlutter ];
168+ [engine.lifecycleChannel setMessageHandler: ^(id message, FlutterReply callback) {
169+ if (lifecycleExpectations.count == 0 ) {
170+ XCTFail (@" Unexpected lifecycle transition: %@ " , message);
171+ return ;
172+ }
173+ XCAppLifecycleTestExpectation* nextExpectation = [lifecycleExpectations objectAtIndex: 0 ];
174+ if (![[nextExpectation expectedLifecycle ] isEqualToString: message]) {
175+ XCTFail (@" Expected lifecycle %@ but instead received %@ " , [nextExpectation expectedLifecycle ],
176+ message);
177+ return ;
178+ }
179+
180+ [nextExpectation fulfill ];
181+ [lifecycleExpectations removeObjectAtIndex: 0 ];
182+ }];
183+
184+ [self waitForExpectations: lifecycleExpectations timeout: 5 ];
185+
186+ // Now put the FlutterViewController into background.
187+ [lifecycleExpectations addObjectsFromArray: @[
188+ [[XCAppLifecycleTestExpectation alloc ]
189+ initForLifecycle: @" AppLifecycleState.inactive"
190+ forStep: @" putting FlutterViewController to the background" ],
191+ [[XCAppLifecycleTestExpectation alloc ]
192+ initForLifecycle: @" AppLifecycleState.paused"
193+ forStep: @" putting FlutterViewController to the background" ]
194+ ]];
195+ [[NSNotificationCenter defaultCenter ]
196+ postNotificationName: UIApplicationWillResignActiveNotification
197+ object: nil ];
198+ [[NSNotificationCenter defaultCenter ]
199+ postNotificationName: UIApplicationDidEnterBackgroundNotification
200+ object: nil ];
201+ [self waitForExpectations: lifecycleExpectations timeout: 5 ];
202+
203+ // Now restore to foreground
204+ [lifecycleExpectations addObjectsFromArray: @[
205+ [[XCAppLifecycleTestExpectation alloc ]
206+ initForLifecycle: @" AppLifecycleState.inactive"
207+ forStep: @" putting FlutterViewController back to foreground" ],
208+ [[XCAppLifecycleTestExpectation alloc ]
209+ initForLifecycle: @" AppLifecycleState.resumed"
210+ forStep: @" putting FlutterViewController back to foreground" ]
211+ ]];
212+ [[NSNotificationCenter defaultCenter ]
213+ postNotificationName: UIApplicationWillEnterForegroundNotification
214+ object: nil ];
215+ [[NSNotificationCenter defaultCenter ]
216+ postNotificationName: UIApplicationDidBecomeActiveNotification
217+ object: nil ];
126218 [self waitForExpectations: lifecycleExpectations timeout: 5 ];
127- expectedStates = @[
128- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed" , @" AppLifecycleState.inactive" ,
129- @" AppLifecycleState.paused" ,
130-
131- // The production code currently misbehaves. https://github.com/flutter/flutter/issues/37226.
132- // It will be fixed in a next PR that removes the wrong asserts.
133- @" AppLifecycleState.inactive" , @" AppLifecycleState.paused" , @" AppLifecycleState.inactive" ,
134- @" AppLifecycleState.resumed" ,
135-
136- // We only added 2 from re-launching the FlutterViewController
137- // and none from the background-foreground cycle.
138- @" AppLifecycleState.inactive" , @" AppLifecycleState.resumed"
139- ];
140- XCTAssertEqualObjects (
141- lifecycleEvents, expectedStates,
142- @" AppLifecycleState transitions while presenting a second time not as expected" );
143219
144220 // Dismantle.
145221 [engine.lifecycleChannel setMessageHandler: nil ];
146222 [flutterVC dismissViewControllerAnimated: NO completion: nil ];
147- flutterVC = nil ;
148223 [engine setViewController: nil ];
149- [rootVC dismissViewControllerAnimated: NO completion: nil ];
150- rootVC = nil ;
151224}
225+
152226@end
0 commit comments