| Flutter iOS Embedder |

FlutterPluginAppLifeCycleDelegateTest.mm
Go to the documentation of this file.
1 // Copyright 2013 The Flutter Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 #import <OCMock/OCMock.h>
6 #import <XCTest/XCTest.h>
7
8 #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h"
9 #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterPluginAppLifeCycleDelegate.h"
10 #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterSceneLifeCycle.h"
11 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterPluginAppLifeCycleDelegate_internal.h"
12 #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterSharedApplication.h"
13
14 FLUTTER_ASSERT_ARC
15
16 // --- Test Category to avoid modifying the original production source code ---
17 @interface FlutterPluginAppLifeCycleDelegate (TestUtils)
18 - (void)removeDelegate:(NSObject<FlutterApplicationLifeCycleDelegate>*)delegate;
19 @end
20
21 @implementation FlutterPluginAppLifeCycleDelegate (TestUtils)
22 - (void)removeDelegate:(NSObject<FlutterApplicationLifeCycleDelegate>*)delegate {
23 // Access the private _delegates member via Key-Value Coding (KVC)
24 NSPointerArray* delegates = [self valueForKey:@"_delegates"];
25 for (NSUInteger i = 0; i < delegates.count; i++) {
26 if ([delegates pointerAtIndex:i] == (__bridge void*)delegate) {
27 [delegates removePointerAtIndex:i];
28 break;
29 }
30 }
31 }
32 @end
33 // -----------------------------------------------------------------------
34
35 @protocol TestFlutterPluginWithSceneEvents <NSObject,
36 FlutterApplicationLifeCycleDelegate,
37 FlutterSceneLifeCycleDelegate>
38 @end
39
40 @interface FakeTestFlutterPluginWithSceneEvents : NSObject <TestFlutterPluginWithSceneEvents>
41 @end
42
43 @implementation FakeTestFlutterPluginWithSceneEvents
44 - (BOOL)application:(UIApplication*)application
45 didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
46 return NO;
47 }
48
49 - (BOOL)application:(UIApplication*)application
50 openURL:(NSURL*)url
51 options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
52 return YES;
53 }
54
55 - (BOOL)application:(UIApplication*)application
56 continueUserActivity:(NSUserActivity*)userActivity
57 restorationHandler:(void (^)(NSArray*))restorationHandler {
58 return YES;
59 }
60
61 - (BOOL)application:(UIApplication*)application
62 performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
63 completionHandler:(void (^)(BOOL succeeded))completionHandler
64 API_AVAILABLE(ios(9.0)) {
65 return YES;
66 }
67 @end
68
69 @interface FakePlugin : NSObject <FlutterApplicationLifeCycleDelegate>
70 @end
71
72 @implementation FakePlugin
73 - (BOOL)application:(UIApplication*)application
74 openURL:(NSURL*)url
75 options:(NSDictionary<UIApplicationOpenURLOptionsKey, id>*)options {
76 return YES;
77 }
78
79 - (BOOL)application:(UIApplication*)application
80 continueUserActivity:(NSUserActivity*)userActivity
81 restorationHandler:(void (^)(NSArray*))restorationHandler {
82 return YES;
83 }
84
85 - (BOOL)application:(UIApplication*)application
86 performActionForShortcutItem:(UIApplicationShortcutItem*)shortcutItem
87 completionHandler:(void (^)(BOOL succeeded))completionHandler
88 API_AVAILABLE(ios(9.0)) {
89 return YES;
90 }
91
92 - (BOOL)application:(UIApplication*)application
93 didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
94 return YES;
95 }
96
97 - (BOOL)application:(UIApplication*)application
98 willFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
99 return YES;
100 }
101 @end
102
103 /**
104 * A mock plugin that simulates behavior causing a mutation crash.
105 * This represents a "downstream" mutation where a plugin adds or removes
106 * delegates during a lifecycle notification loop.
107 */
108 @interface MutatingPlugin : NSObject <FlutterApplicationLifeCycleDelegate>
109 @property(nonatomic, weak) FlutterPluginAppLifeCycleDelegate* lifecycleDelegate;
110 @property(nonatomic, assign) BOOL shouldAdd; // YES = Add, NO = Remove
111 @end
112
113 @implementation MutatingPlugin
114 - (BOOL)application:(UIApplication*)application
115 didFinishLaunchingWithOptions:(NSDictionary*)launchOptions {
116 if (self.shouldAdd) {
117 // Case 1: Add a new delegate during the loop over _delegates
118 [self.lifecycleDelegate addDelegate:[[FakePlugin alloc] init]];
119 } else {
120 // Case 2: Remove itself during the loop over _delegates via TestUtils category
121 [(id)self.lifecycleDelegate removeDelegate:self];
122 }
123 return YES;
124 }
125 @end
126
127 @interface FlutterPluginAppLifeCycleDelegateTest : XCTestCase
128 @end
129
130 @implementation FlutterPluginAppLifeCycleDelegateTest
131
132 - (void)testCreate {
133 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
134 XCTAssertNotNil(delegate);
135 }
136
137 - (void)testSceneWillConnectFallback {
138 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
139 id plugin = [[FakePlugin alloc] init];
140 id mockPlugin = OCMPartialMock(plugin);
141 [delegate addDelegate:mockPlugin];
142
143 id mockOptions = OCMClassMock([UISceneConnectionOptions class]);
144 id mockShortcutItem = OCMClassMock([UIApplicationShortcutItem class]);
145 OCMStub([mockOptions shortcutItem]).andReturn(mockShortcutItem);
146 OCMStub([mockOptions sourceApplication]).andReturn(@"bundle_id");
147 id urlContext = OCMClassMock([UIOpenURLContext class]);
148 NSURL* url = [NSURL URLWithString:@"http://example.com"];
149 OCMStub([urlContext URL]).andReturn(url);
150 NSSet<UIOpenURLContext*>* urlContexts = [NSSet setWithObjects:urlContext, nil];
151 OCMStub([mockOptions URLContexts]).andReturn(urlContexts);
152
153 NSDictionary<UIApplicationOpenURLOptionsKey, id>* expectedApplicationOptions = @{
154 UIApplicationLaunchOptionsShortcutItemKey : mockShortcutItem,
155 UIApplicationLaunchOptionsSourceApplicationKey : @"bundle_id",
156 UIApplicationLaunchOptionsURLKey : url,
157 };
158
159 [delegate sceneWillConnectFallback:mockOptions];
160 OCMVerify([mockPlugin application:[UIApplication sharedApplication]
161 didFinishLaunchingWithOptions:expectedApplicationOptions]);
162 }
163
164 - (void)testSceneWillConnectFallbackSkippedSupportsScenes {
165 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
166 id plugin = [[FakeTestFlutterPluginWithSceneEvents alloc] init];
167 id mockPlugin = OCMPartialMock(plugin);
168 [delegate addDelegate:mockPlugin];
169
170 id mockOptions = OCMClassMock([UISceneConnectionOptions class]);
171 id mockShortcutItem = OCMClassMock([UIApplicationShortcutItem class]);
172 OCMStub([mockOptions shortcutItem]).andReturn(mockShortcutItem);
173 OCMStub([mockOptions sourceApplication]).andReturn(@"bundle_id");
174 id urlContext = OCMClassMock([UIOpenURLContext class]);
175 NSURL* url = [NSURL URLWithString:@"http://example.com"];
176 OCMStub([urlContext URL]).andReturn(url);
177 NSSet<UIOpenURLContext*>* urlContexts = [NSSet setWithObjects:urlContext, nil];
178 OCMStub([mockOptions URLContexts]).andReturn(urlContexts);
179
180 [delegate sceneWillConnectFallback:mockOptions];
181 OCMReject([mockPlugin application:[OCMArg any] didFinishLaunchingWithOptions:[OCMArg any]]);
182 }
183
184 - (void)testSceneWillConnectFallbackSkippedNoOptions {
185 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
186 id plugin = [[FakePlugin alloc] init];
187 id mockPlugin = OCMPartialMock(plugin);
188 [delegate addDelegate:mockPlugin];
189
190 id mockOptions = OCMClassMock([UISceneConnectionOptions class]);
191
192 [delegate sceneWillConnectFallback:mockOptions];
193 OCMReject([mockPlugin application:[OCMArg any] didFinishLaunchingWithOptions:[OCMArg any]]);
194 }
195
196 - (void)testDidEnterBackground {
197 XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc]
198 initWithName:UIApplicationDidEnterBackgroundNotification];
199 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
200 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
201 [delegate addDelegate:plugin];
202 [[NSNotificationCenter defaultCenter]
203 postNotificationName:UIApplicationDidEnterBackgroundNotification
204 object:nil];
205
206 [self waitForExpectations:@[ expectation ] timeout:5.0];
207 OCMVerify([plugin applicationDidEnterBackground:[UIApplication sharedApplication]]);
208 }
209
210 - (void)testDidEnterBackgroundWithUIScene {
211 XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc]
212 initWithName:UIApplicationDidEnterBackgroundNotification];
213 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
214 id mockApplication = OCMClassMock([FlutterSharedApplication class]);
215 OCMStub([mockApplication hasSceneDelegate]).andReturn(YES);
216 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
217 [delegate addDelegate:plugin];
218 [[NSNotificationCenter defaultCenter]
219 postNotificationName:UIApplicationDidEnterBackgroundNotification
220 object:nil];
221
222 [self waitForExpectations:@[ expectation ] timeout:5.0];
223 OCMReject([plugin applicationDidEnterBackground:[OCMArg any]]);
224 }
225
226 - (void)testSceneDidEnterBackgroundFallback {
227 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
228 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
229 [delegate addDelegate:plugin];
230
231 [delegate sceneDidEnterBackgroundFallback];
232 OCMVerify([plugin applicationDidEnterBackground:[UIApplication sharedApplication]]);
233 }
234
235 - (void)testUnnecessarySceneDidEnterBackgroundFallback {
236 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
237 id plugin = OCMProtocolMock(@protocol(TestFlutterPluginWithSceneEvents));
238 [delegate addDelegate:plugin];
239
240 [delegate sceneDidEnterBackgroundFallback];
241 OCMReject([plugin applicationDidEnterBackground:[OCMArg any]]);
242 }
243
244 - (void)testWillEnterForeground {
245 XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc]
246 initWithName:UIApplicationWillEnterForegroundNotification];
247
248 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
249 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
250 [delegate addDelegate:plugin];
251 [[NSNotificationCenter defaultCenter]
252 postNotificationName:UIApplicationWillEnterForegroundNotification
253 object:nil];
254 [self waitForExpectations:@[ expectation ] timeout:5.0];
255 OCMVerify([plugin applicationWillEnterForeground:[UIApplication sharedApplication]]);
256 }
257
258 - (void)testWillEnterForegroundWithUIScene {
259 XCTNSNotificationExpectation* expectation = [[XCTNSNotificationExpectation alloc]
260 initWithName:UIApplicationWillEnterForegroundNotification];
261 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
262 id mockApplication = OCMClassMock([FlutterSharedApplication class]);
263 OCMStub([mockApplication hasSceneDelegate]).andReturn(YES);
264 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
265 [delegate addDelegate:plugin];
266 [[NSNotificationCenter defaultCenter]
267 postNotificationName:UIApplicationWillEnterForegroundNotification
268 object:nil];
269
270 [self waitForExpectations:@[ expectation ] timeout:5.0];
271 OCMReject([plugin applicationWillEnterForeground:[OCMArg any]]);
272 }
273
274 - (void)testSceneWillEnterForegroundFallback {
275 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
276 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
277 [delegate addDelegate:plugin];
278
279 [delegate sceneWillEnterForegroundFallback];
280 OCMVerify([plugin applicationWillEnterForeground:[UIApplication sharedApplication]]);
281 }
282
283 - (void)testUnnecessarySceneWillEnterForegroundFallback {
284 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
285 id plugin = OCMProtocolMock(@protocol(TestFlutterPluginWithSceneEvents));
286 [delegate addDelegate:plugin];
287
288 [delegate sceneWillEnterForegroundFallback];
289 OCMReject([plugin applicationWillEnterForeground:[OCMArg any]]);
290 }
291
292 - (void)testWillResignActive {
293 XCTNSNotificationExpectation* expectation =
294 [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationWillResignActiveNotification];
295
296 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
297 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
298 [delegate addDelegate:plugin];
299 [[NSNotificationCenter defaultCenter]
300 postNotificationName:UIApplicationWillResignActiveNotification
301 object:nil];
302 [self waitForExpectations:@[ expectation ] timeout:5.0];
303 OCMVerify([plugin applicationWillResignActive:[UIApplication sharedApplication]]);
304 }
305
306 - (void)testWillResignActiveWithUIScene {
307 XCTNSNotificationExpectation* expectation =
308 [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationWillResignActiveNotification];
309 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
310 id mockApplication = OCMClassMock([FlutterSharedApplication class]);
311 OCMStub([mockApplication hasSceneDelegate]).andReturn(YES);
312 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
313 [delegate addDelegate:plugin];
314 [[NSNotificationCenter defaultCenter]
315 postNotificationName:UIApplicationWillResignActiveNotification
316 object:nil];
317
318 [self waitForExpectations:@[ expectation ] timeout:5.0];
319 OCMReject([plugin applicationWillResignActive:[OCMArg any]]);
320 }
321
322 - (void)testSceneWillResignActiveFallback {
323 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
324 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
325 [delegate addDelegate:plugin];
326
327 [delegate sceneWillResignActiveFallback];
328 OCMVerify([plugin applicationWillResignActive:[UIApplication sharedApplication]]);
329 }
330
331 - (void)testUnnecessarySceneWillResignActiveFallback {
332 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
333 id plugin = OCMProtocolMock(@protocol(TestFlutterPluginWithSceneEvents));
334 [delegate addDelegate:plugin];
335
336 [delegate sceneWillResignActiveFallback];
337 OCMReject([plugin applicationWillResignActive:[OCMArg any]]);
338 }
339
340 - (void)testDidBecomeActive {
341 XCTNSNotificationExpectation* expectation =
342 [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationDidBecomeActiveNotification];
343
344 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
345 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
346 [delegate addDelegate:plugin];
347 [[NSNotificationCenter defaultCenter]
348 postNotificationName:UIApplicationDidBecomeActiveNotification
349 object:nil];
350 [self waitForExpectations:@[ expectation ] timeout:5.0];
351 OCMVerify([plugin applicationDidBecomeActive:[UIApplication sharedApplication]]);
352 }
353
354 - (void)testDidBecomeActiveWithUIScene {
355 XCTNSNotificationExpectation* expectation =
356 [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationDidBecomeActiveNotification];
357 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
358 id mockApplication = OCMClassMock([FlutterSharedApplication class]);
359 OCMStub([mockApplication hasSceneDelegate]).andReturn(YES);
360 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
361 [delegate addDelegate:plugin];
362 [[NSNotificationCenter defaultCenter]
363 postNotificationName:UIApplicationDidBecomeActiveNotification
364 object:nil];
365
366 [self waitForExpectations:@[ expectation ] timeout:5.0];
367 OCMReject([plugin applicationDidBecomeActive:[OCMArg any]]);
368 }
369
370 - (void)testSceneDidBecomeActiveFallback {
371 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
372 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
373 [delegate addDelegate:plugin];
374
375 [delegate sceneDidBecomeActiveFallback];
376 OCMVerify([plugin applicationDidBecomeActive:[UIApplication sharedApplication]]);
377 }
378
379 - (void)testUnnecessarySceneDidBecomeActiveFallback {
380 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
381 id plugin = OCMProtocolMock(@protocol(TestFlutterPluginWithSceneEvents));
382 [delegate addDelegate:plugin];
383
384 [delegate sceneDidBecomeActiveFallback];
385 OCMReject([plugin applicationDidBecomeActive:[OCMArg any]]);
386 }
387
388 - (void)testSceneFallbackOpenURLContexts {
389 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
390 id plugin = [[FakePlugin alloc] init];
391 id mockPlugin = OCMPartialMock(plugin);
392 [delegate addDelegate:mockPlugin];
393
394 id urlContext = OCMClassMock([UIOpenURLContext class]);
395 NSURL* url = [NSURL URLWithString:@"http://example.com"];
396 OCMStub([urlContext URL]).andReturn(url);
397 NSSet<UIOpenURLContext*>* urlContexts = [NSSet setWithObjects:urlContext, nil];
398
399 NSDictionary<UIApplicationOpenURLOptionsKey, id>* expectedApplicationOptions = @{
400 UIApplicationOpenURLOptionsOpenInPlaceKey : @(NO),
401 };
402
403 [delegate sceneFallbackOpenURLContexts:urlContexts];
404 OCMVerify([mockPlugin application:[UIApplication sharedApplication]
405 openURL:url
406 options:expectedApplicationOptions]);
407 }
408
409 - (void)testConvertURLOptions {
410 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
411 id plugin = [[FakePlugin alloc] init];
412 id mockPlugin = OCMPartialMock(plugin);
413 [delegate addDelegate:mockPlugin];
414
415 NSString* bundleId = @"app.bundle.id";
416 id annotation = @{@"key" : @"value"};
417 id eventAttribution = OCMClassMock([UIEventAttribution class]);
418
419 UIOpenURLContext* urlContext = OCMClassMock([UIOpenURLContext class]);
420 NSURL* url = [NSURL URLWithString:@"http://example.com"];
421 OCMStub([urlContext URL]).andReturn(url);
422 id sceneOptions = OCMClassMock([UISceneOpenURLOptions class]);
423 OCMStub([sceneOptions sourceApplication]).andReturn(bundleId);
424 OCMStub([sceneOptions annotation]).andReturn(annotation);
425 OCMStub([sceneOptions openInPlace]).andReturn(YES);
426 OCMStub([sceneOptions eventAttribution]).andReturn(eventAttribution);
427
428 OCMStub([urlContext options]).andReturn(sceneOptions);
429 NSSet<UIOpenURLContext*>* urlContexts = [NSSet setWithObjects:urlContext, nil];
430
431 [delegate sceneFallbackOpenURLContexts:urlContexts];
432
433 NSDictionary<UIApplicationOpenURLOptionsKey, id>* expectedApplicationOptions = @{
434 UIApplicationOpenURLOptionsSourceApplicationKey : bundleId,
435 UIApplicationOpenURLOptionsAnnotationKey : annotation,
436 UIApplicationOpenURLOptionsOpenInPlaceKey : @(YES),
437 UIApplicationOpenURLOptionsEventAttributionKey : eventAttribution,
438 };
439
440 OCMVerify([mockPlugin application:[UIApplication sharedApplication]
441 openURL:url
442 options:expectedApplicationOptions]);
443 }
444
445 - (void)testUnnecessarySceneFallbackOpenURLContexts {
446 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
447 id plugin = [[FakeTestFlutterPluginWithSceneEvents alloc] init];
448 id mockPlugin = OCMPartialMock(plugin);
449 [delegate addDelegate:mockPlugin];
450
451 id urlContext = OCMClassMock([UIOpenURLContext class]);
452 NSSet<UIOpenURLContext*>* urlContexts = [NSSet setWithObjects:urlContext, nil];
453
454 [delegate sceneFallbackOpenURLContexts:urlContexts];
455 OCMReject([mockPlugin application:[OCMArg any] openURL:[OCMArg any] options:[OCMArg any]]);
456 }
457
458 - (void)testSceneFallbackContinueUserActivity {
459 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
460 id plugin = [[FakePlugin alloc] init];
461 id mockPlugin = OCMPartialMock(plugin);
462 [delegate addDelegate:mockPlugin];
463
464 id userActivity = OCMClassMock([NSUserActivity class]);
465
466 [delegate sceneFallbackContinueUserActivity:userActivity];
467 OCMVerify([mockPlugin application:[UIApplication sharedApplication]
468 continueUserActivity:userActivity
469 restorationHandler:[OCMArg any]]);
470 }
471
472 - (void)testUnnecessarySceneFallbackContinueUserActivity {
473 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
474 id plugin = [[FakeTestFlutterPluginWithSceneEvents alloc] init];
475 id mockPlugin = OCMPartialMock(plugin);
476 [delegate addDelegate:mockPlugin];
477
478 id userActivity = OCMClassMock([NSUserActivity class]);
479
480 [delegate sceneFallbackContinueUserActivity:userActivity];
481 OCMReject([mockPlugin application:[UIApplication sharedApplication]
482 continueUserActivity:userActivity
483 restorationHandler:[OCMArg any]]);
484 }
485
486 - (void)testSceneFallbackPerformActionForShortcutItem {
487 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
488 FakePlugin* plugin = [[FakePlugin alloc] init];
489 FakePlugin* mockPlugin = OCMPartialMock(plugin);
490 [delegate addDelegate:mockPlugin];
491
492 id shortcut = OCMClassMock([UIApplicationShortcutItem class]);
493 id handler = ^(BOOL succeeded) {
494 };
495
496 [delegate sceneFallbackPerformActionForShortcutItem:shortcut completionHandler:handler];
497 OCMVerify([mockPlugin application:[UIApplication sharedApplication]
498 performActionForShortcutItem:shortcut
499 completionHandler:handler]);
500 }
501
502 - (void)testUnnecessarySceneFallbackPerformActionForShortcutItem {
503 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
504 FakeTestFlutterPluginWithSceneEvents* plugin =
505 [[FakeTestFlutterPluginWithSceneEvents alloc] init];
506 FakeTestFlutterPluginWithSceneEvents* mockPlugin = OCMPartialMock(plugin);
507 [delegate addDelegate:mockPlugin];
508
509 id shortcut = OCMClassMock([UIApplicationShortcutItem class]);
510 [delegate sceneFallbackPerformActionForShortcutItem:shortcut
511 completionHandler:^(BOOL succeeded){
512 }];
513 OCMReject([mockPlugin application:[OCMArg any]
514 performActionForShortcutItem:[OCMArg any]
515 completionHandler:[OCMArg any]]);
516 }
517
518 - (void)testWillTerminate {
519 XCTNSNotificationExpectation* expectation =
520 [[XCTNSNotificationExpectation alloc] initWithName:UIApplicationWillTerminateNotification];
521
522 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
523 id plugin = OCMProtocolMock(@protocol(FlutterPlugin));
524 [delegate addDelegate:plugin];
525 [[NSNotificationCenter defaultCenter] postNotificationName:UIApplicationWillTerminateNotification
526 object:nil];
527 [self waitForExpectations:@[ expectation ] timeout:5.0];
528 OCMVerify([plugin applicationWillTerminate:[UIApplication sharedApplication]]);
529 }
530
531 - (void)testReleasesPluginOnDealloc {
532 __weak id<FlutterApplicationLifeCycleDelegate> weakPlugin;
533 __weak FlutterPluginAppLifeCycleDelegate* weakDelegate;
534 @autoreleasepool {
535 FakePlugin* fakePlugin = [[FakePlugin alloc] init];
536 weakPlugin = fakePlugin;
537 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
538 [delegate addDelegate:fakePlugin];
539 weakDelegate = delegate;
540 }
541 XCTAssertNil(weakPlugin);
542 XCTAssertNil(weakDelegate);
543 }
544
545 - (void)testApplicationWillFinishLaunchingSceneFallbackForwards {
546 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
547 id plugin = [[FakePlugin alloc] init];
548 id mockPlugin = OCMPartialMock(plugin);
549 [delegate addDelegate:mockPlugin];
550 id mockApplication = OCMClassMock([UIApplication class]);
551 NSDictionary* options = @{};
552
553 [delegate sceneFallbackWillFinishLaunchingApplication:mockApplication];
554 OCMVerify(times(1), [mockPlugin application:mockApplication
555 willFinishLaunchingWithOptions:options]);
556 }
557
558 - (void)testApplicationWillFinishLaunchingSceneFallbackNoForwardAfterWillLaunch {
559 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
560 id plugin = [[FakePlugin alloc] init];
561 id mockPlugin = OCMPartialMock(plugin);
562 [delegate addDelegate:mockPlugin];
563 id mockApplication = OCMClassMock([UIApplication class]);
564 NSDictionary* options = @{@"key" : @"value"};
565
566 [delegate application:mockApplication willFinishLaunchingWithOptions:options];
567 [delegate sceneFallbackWillFinishLaunchingApplication:mockApplication];
568 OCMVerify(times(1), [mockPlugin application:mockApplication
569 willFinishLaunchingWithOptions:options]);
570 }
571
572 - (void)testApplicationWillFinishLaunchingSceneFallbackNoForwardAfterDidLaunch {
573 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
574 id plugin = [[FakePlugin alloc] init];
575 id mockPlugin = OCMPartialMock(plugin);
576 [delegate addDelegate:mockPlugin];
577 id mockApplication = OCMClassMock([UIApplication class]);
578 NSDictionary* options = @{@"key" : @"value"};
579
580 [delegate application:mockApplication didFinishLaunchingWithOptions:options];
581 [delegate sceneFallbackWillFinishLaunchingApplication:mockApplication];
582 OCMVerify(times(0), [mockPlugin application:mockApplication
583 willFinishLaunchingWithOptions:options]);
584 }
585
586 - (void)testApplicationDidFinishLaunchingSceneFallbackForwards {
587 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
588 id plugin = [[FakePlugin alloc] init];
589 id mockPlugin = OCMPartialMock(plugin);
590 [delegate addDelegate:mockPlugin];
591 id mockApplication = OCMClassMock([UIApplication class]);
592 NSDictionary* options = @{};
593
594 [delegate sceneFallbackDidFinishLaunchingApplication:mockApplication];
595 OCMVerify(times(1), [mockPlugin application:mockApplication
596 didFinishLaunchingWithOptions:options]);
597 }
598
599 - (void)testApplicationDidFinishLaunchingSceneFallbackNoForward {
600 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
601 id plugin = [[FakePlugin alloc] init];
602 id mockPlugin = OCMPartialMock(plugin);
603 [delegate addDelegate:mockPlugin];
604 id mockApplication = OCMClassMock([UIApplication class]);
605 NSDictionary* options = @{@"key" : @"value"};
606
607 [delegate application:mockApplication didFinishLaunchingWithOptions:options];
608 [delegate sceneFallbackDidFinishLaunchingApplication:mockApplication];
609 OCMVerify(times(1), [mockPlugin application:mockApplication
610 didFinishLaunchingWithOptions:options]);
611 }
612
613 - (void)testCanAddDelegateDuringEnumeration {
614 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
615 MutatingPlugin* mutatingPlugin = [[MutatingPlugin alloc] init];
616 mutatingPlugin.lifecycleDelegate = delegate;
617 mutatingPlugin.shouldAdd = YES; // Add Mode
618
619 [delegate addDelegate:mutatingPlugin];
620
621 // Validation that [_delegates allObjects] (snapshotting) prevents NSGenericException crash
622 // when a plugin adds another plugin during the dispatch loop.
623 BOOL result = [delegate application:[UIApplication sharedApplication]
624 didFinishLaunchingWithOptions:@{}];
625
626 XCTAssertTrue(result);
627 }
628
629 - (void)testCanRemoveSelfDuringEnumeration {
630 FlutterPluginAppLifeCycleDelegate* delegate = [[FlutterPluginAppLifeCycleDelegate alloc] init];
631 MutatingPlugin* mutatingPlugin = [[MutatingPlugin alloc] init];
632 mutatingPlugin.lifecycleDelegate = delegate;
633 mutatingPlugin.shouldAdd = NO; // Delete Mode
634
635 [delegate addDelegate:mutatingPlugin];
636
637 // Validation that [_delegates allObjects] (snapshotting) prevents crash
638 // when a plugin removes itself via removeDelegate during the dispatch loop.
639 BOOL result = [delegate application:[UIApplication sharedApplication]
640 didFinishLaunchingWithOptions:@{}];
641
642 XCTAssertTrue(result);
643 }
644
645 @end
FlutterMacros.h
FlutterPluginAppLifeCycleDelegate.h
FlutterPluginAppLifeCycleDelegate_internal.h
FlutterSceneLifeCycle.h
FlutterSharedApplication.h
API_AVAILABLE
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
FlutterPluginAppLifeCycleDelegate(TestUtils)
Definition: FlutterPluginAppLifeCycleDelegateTest.mm:17
FakePlugin
Definition: FlutterPluginAppLifeCycleDelegateTest.mm:69
FakeTestFlutterPluginWithSceneEvents
Definition: FlutterPluginAppLifeCycleDelegateTest.mm:40
FlutterPluginAppLifeCycleDelegate
Definition: FlutterPluginAppLifeCycleDelegate.h:16
-[FlutterPluginAppLifeCycleDelegate application:didFinishLaunchingWithOptions:]
BOOL application:didFinishLaunchingWithOptions:(UIApplication *application,[didFinishLaunchingWithOptions] NSDictionary *launchOptions)
Definition: FlutterPluginAppLifeCycleDelegate.mm:146
-[FlutterPluginAppLifeCycleDelegate application:willFinishLaunchingWithOptions:]
BOOL application:willFinishLaunchingWithOptions:(UIApplication *application,[willFinishLaunchingWithOptions] NSDictionary *launchOptions)
Definition: FlutterPluginAppLifeCycleDelegate.mm:222
-[FlutterPluginAppLifeCycleDelegate addDelegate:]
void addDelegate:(NSObject< FlutterApplicationLifeCycleDelegate > *delegate)
Definition: FlutterPluginAppLifeCycleDelegate.mm:114
FlutterPluginAppLifeCycleDelegateTest
Definition: FlutterPluginAppLifeCycleDelegateTest.mm:128
FlutterSharedApplication
Definition: FlutterSharedApplication.h:13
MutatingPlugin
Definition: FlutterPluginAppLifeCycleDelegateTest.mm:108
MutatingPlugin::shouldAdd
BOOL shouldAdd
Definition: FlutterPluginAppLifeCycleDelegateTest.mm:110
MutatingPlugin::lifecycleDelegate
FlutterPluginAppLifeCycleDelegate * lifecycleDelegate
Definition: FlutterPluginAppLifeCycleDelegateTest.mm:109
FLUTTER_ASSERT_ARC
Definition: FlutterChannelKeyResponder.mm:13
FlutterApplicationLifeCycleDelegate-p
Definition: FlutterPlugin.h:26
FlutterPlugin-p
Definition: FlutterPlugin.h:190
FlutterSceneLifeCycleDelegate-p
Definition: FlutterSceneLifeCycle.h:31
TestFlutterPluginWithSceneEvents-p
Definition: FlutterEngineTest.mm:24