Flutter Engine
 
Loading...
Searching...
No Matches
FlutterViewControllerTest.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
30
32
33using namespace flutter::testing;
34
35/// Sometimes we have to use a custom mock to avoid retain cycles in OCMock.
36/// Used for testing low memory notification.
38
39@property(nonatomic, strong) FlutterBasicMessageChannel* lifecycleChannel;
40@property(nonatomic, strong) FlutterBasicMessageChannel* keyEventChannel;
41@property(nonatomic, weak) FlutterViewController* viewController;
42@property(nonatomic, strong) FlutterTextInputPlugin* textInputPlugin;
43@property(nonatomic, assign) BOOL didCallNotifyLowMemory;
44
46
47- (void)sendKeyEvent:(const FlutterKeyEvent&)event
48 callback:(nullable FlutterKeyEventCallback)callback
49 userData:(nullable void*)userData;
50@end
51
52@implementation FlutterEnginePartialMock
53
54// Synthesize properties declared readonly in FlutterEngine.
55@synthesize lifecycleChannel;
56@synthesize keyEventChannel;
57@synthesize viewController;
58@synthesize textInputPlugin;
59
60- (void)notifyLowMemory {
61 _didCallNotifyLowMemory = YES;
62}
63
64- (void)sendKeyEvent:(const FlutterKeyEvent&)event
65 callback:(FlutterKeyEventCallback)callback
66 userData:(void*)userData API_AVAILABLE(ios(9.0)) {
67 if (callback == nil) {
68 return;
69 }
70 // NSAssert(callback != nullptr, @"Invalid callback");
71 // Response is async, so we have to post it to the run loop instead of calling
72 // it directly.
73 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
74 ^() {
75 callback(true, userData);
76 });
77}
78@end
79
80@interface FlutterEngine ()
81- (BOOL)createShell:(NSString*)entrypoint
82 libraryURI:(NSString*)libraryURI
83 initialRoute:(NSString*)initialRoute;
84- (void)dispatchPointerDataPacket:(std::unique_ptr<flutter::PointerDataPacket>)packet;
85- (void)updateViewportMetrics:(flutter::ViewportMetrics)viewportMetrics;
86- (void)attachView;
87@end
88
89@interface FlutterEngine (TestLowMemory)
90- (void)notifyLowMemory;
91@end
92
93extern NSNotificationName const FlutterViewControllerWillDealloc;
94
95/// A simple mock class for FlutterEngine.
96///
97/// OCMClassMock can't be used for FlutterEngine sometimes because OCMock retains arguments to
98/// invocations and since the init for FlutterViewController calls a method on the
99/// FlutterEngine it creates a retain cycle that stops us from testing behaviors related to
100/// deleting FlutterViewControllers.
101///
102/// Used for testing deallocation.
103@interface MockEngine : NSObject
104@property(nonatomic, strong) FlutterDartProject* project;
105@end
106
107@implementation MockEngine
109 return nil;
110}
111- (void)setViewController:(FlutterViewController*)viewController {
112 // noop
113}
114@end
115
116@interface FlutterKeyboardManager (Tests)
117@property(nonatomic, retain, readonly)
118 NSMutableArray<id<FlutterKeyPrimaryResponder>>* primaryResponders;
119@end
120
121@interface FlutterEmbedderKeyResponder (Tests)
122@property(nonatomic, copy, readonly) FlutterSendKeyEvent sendEvent;
123@end
124
125@interface NSObject (Tests)
126@property(nonatomic, strong) FlutterEngine* mockLaunchEngine;
127@end
128
129@interface FlutterViewController (Tests)
130
131@property(nonatomic, assign) double targetViewInsetBottom;
132@property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground;
133@property(nonatomic, assign) BOOL keyboardAnimationIsShowing;
134@property(nonatomic, strong) VSyncClient* keyboardAnimationVSyncClient;
135@property(nonatomic, strong) VSyncClient* touchRateCorrectionVSyncClient;
136@property(nonatomic, assign) BOOL awokenFromNib;
137
138- (void)createTouchRateCorrectionVSyncClientIfNeeded;
139- (void)surfaceUpdated:(BOOL)appeared;
140- (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences;
141- (void)handlePressEvent:(FlutterUIPressProxy*)press
142 nextAction:(void (^)())next API_AVAILABLE(ios(13.4));
143- (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer;
144- (void)updateViewportMetricsIfNeeded;
145- (void)onUserSettingsChanged:(NSNotification*)notification;
146- (void)applicationWillTerminate:(NSNotification*)notification;
147- (void)goToApplicationLifecycle:(nonnull NSString*)state;
148- (void)handleKeyboardNotification:(NSNotification*)notification;
149- (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(int)keyboardMode;
150- (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification;
151- (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification;
152- (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame;
153- (void)startKeyBoardAnimation:(NSTimeInterval)duration;
154- (void)hideKeyboardImmediately;
155- (UIView*)keyboardAnimationView;
156- (SpringAnimation*)keyboardSpringAnimation;
157- (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation;
158- (void)setUpKeyboardAnimationVsyncClient:
159 (FlutterKeyboardAnimationCallback)keyboardAnimationCallback;
160- (void)ensureViewportMetricsIsCorrect;
161- (void)invalidateKeyboardAnimationVSyncClient;
162- (void)addInternalPlugins;
163- (flutter::PointerData)generatePointerDataForFake;
164- (void)sharedSetupWithProject:(nullable FlutterDartProject*)project
165 initialRoute:(nullable NSString*)initialRoute;
166- (void)applicationBecameActive:(NSNotification*)notification;
167- (void)applicationWillResignActive:(NSNotification*)notification;
168- (void)applicationWillTerminate:(NSNotification*)notification;
169- (void)applicationDidEnterBackground:(NSNotification*)notification;
170- (void)applicationWillEnterForeground:(NSNotification*)notification;
171- (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0));
172- (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0));
173- (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0));
174- (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0));
175- (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0));
176- (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches;
177@end
178
179@interface FlutterViewControllerTest : XCTestCase
180@property(nonatomic, strong) id mockEngine;
181@property(nonatomic, strong) id mockTextInputPlugin;
182@property(nonatomic, strong) id messageSent;
183- (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback;
184@end
185
186@interface UITouch ()
187
188@property(nonatomic, readwrite) UITouchPhase phase;
189
190@end
191
192@interface VSyncClient (Testing)
193
194- (CADisplayLink*)getDisplayLink;
195
196@end
197
198@implementation FlutterViewControllerTest
199
200- (void)setUp {
201 self.mockEngine = OCMClassMock([FlutterEngine class]);
202 self.mockTextInputPlugin = OCMClassMock([FlutterTextInputPlugin class]);
203 OCMStub([self.mockEngine textInputPlugin]).andReturn(self.mockTextInputPlugin);
204 self.messageSent = nil;
205}
206
207- (void)tearDown {
208 // We stop mocking here to avoid retain cycles that stop
209 // FlutterViewControllers from deallocing.
210 [self.mockEngine stopMocking];
211 self.mockEngine = nil;
212 self.mockTextInputPlugin = nil;
213 self.messageSent = nil;
214}
215
216- (id)setUpMockScreen {
217 UIScreen* mockScreen = OCMClassMock([UIScreen class]);
218 // iPhone 14 pixels
219 CGRect screenBounds = CGRectMake(0, 0, 1170, 2532);
220 OCMStub([mockScreen bounds]).andReturn(screenBounds);
221 CGFloat screenScale = 1;
222 OCMStub([mockScreen scale]).andReturn(screenScale);
223
224 return mockScreen;
225}
226
227- (id)setUpMockView:(FlutterViewController*)viewControllerMock
228 screen:(UIScreen*)screen
229 viewFrame:(CGRect)viewFrame
230 convertedFrame:(CGRect)convertedFrame {
231 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
232 id mockView = OCMClassMock([UIView class]);
233 OCMStub([mockView frame]).andReturn(viewFrame);
234 OCMStub([mockView convertRect:viewFrame toCoordinateSpace:[OCMArg any]])
235 .andReturn(convertedFrame);
236 OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView);
237
238 return mockView;
239}
240
241- (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient {
242 FlutterEngine* engine = [[FlutterEngine alloc] init];
243 [engine runWithEntrypoint:nil];
244 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
245 nibName:nil
246 bundle:nil];
247 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
248 [viewControllerMock loadView];
249 [viewControllerMock viewDidLoad];
250 OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]);
251}
252
253- (void)testStartKeyboardAnimationWillInvokeSetupKeyboardSpringAnimationIfNeeded {
254 FlutterEngine* engine = [[FlutterEngine alloc] init];
255 [engine runWithEntrypoint:nil];
256 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
257 nibName:nil
258 bundle:nil];
259 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
260 viewControllerMock.targetViewInsetBottom = 100;
261 [viewControllerMock startKeyBoardAnimation:0.25];
262
263 CAAnimation* keyboardAnimation =
264 [[viewControllerMock keyboardAnimationView].layer animationForKey:@"position"];
265
266 OCMVerify([viewControllerMock setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation]);
267}
268
269- (void)testSetupKeyboardSpringAnimationIfNeeded {
270 FlutterEngine* engine = [[FlutterEngine alloc] init];
271 [engine runWithEntrypoint:nil];
272 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
273 nibName:nil
274 bundle:nil];
275 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
276 UIScreen* screen = [self setUpMockScreen];
277 CGRect viewFrame = screen.bounds;
278 [self setUpMockView:viewControllerMock
279 screen:screen
280 viewFrame:viewFrame
281 convertedFrame:viewFrame];
282
283 // Null check.
284 [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:nil];
285 SpringAnimation* keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation];
286 XCTAssertTrue(keyboardSpringAnimation == nil);
287
288 // CAAnimation that is not a CASpringAnimation.
289 CABasicAnimation* nonSpringAnimation = [CABasicAnimation animation];
290 nonSpringAnimation.duration = 1.0;
291 nonSpringAnimation.fromValue = [NSNumber numberWithFloat:0.0];
292 nonSpringAnimation.toValue = [NSNumber numberWithFloat:1.0];
293 nonSpringAnimation.keyPath = @"position";
294 [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:nonSpringAnimation];
295 keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation];
296
297 XCTAssertTrue(keyboardSpringAnimation == nil);
298
299 // CASpringAnimation.
300 CASpringAnimation* springAnimation = [CASpringAnimation animation];
301 springAnimation.mass = 1.0;
302 springAnimation.stiffness = 100.0;
303 springAnimation.damping = 10.0;
304 springAnimation.keyPath = @"position";
305 springAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)];
306 springAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)];
307 [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:springAnimation];
308 keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation];
309 XCTAssertTrue(keyboardSpringAnimation != nil);
310}
311
312- (void)testKeyboardAnimationIsShowingAndCompounding {
313 FlutterEngine* engine = [[FlutterEngine alloc] init];
314 [engine runWithEntrypoint:nil];
315 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
316 nibName:nil
317 bundle:nil];
318 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
319 UIScreen* screen = [self setUpMockScreen];
320 CGRect viewFrame = screen.bounds;
321 [self setUpMockView:viewControllerMock
322 screen:screen
323 viewFrame:viewFrame
324 convertedFrame:viewFrame];
325
326 BOOL isLocal = YES;
327 CGFloat screenHeight = screen.bounds.size.height;
328 CGFloat screenWidth = screen.bounds.size.height;
329
330 // Start show keyboard animation.
331 CGRect initialShowKeyboardBeginFrame = CGRectMake(0, screenHeight, screenWidth, 250);
332 CGRect initialShowKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500);
333 NSNotification* fakeNotification = [NSNotification
334 notificationWithName:UIKeyboardWillChangeFrameNotification
335 object:nil
336 userInfo:@{
337 @"UIKeyboardFrameBeginUserInfoKey" : @(initialShowKeyboardBeginFrame),
338 @"UIKeyboardFrameEndUserInfoKey" : @(initialShowKeyboardEndFrame),
339 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
340 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
341 }];
342 viewControllerMock.targetViewInsetBottom = 0;
343 [viewControllerMock handleKeyboardNotification:fakeNotification];
344 BOOL isShowingAnimation1 = viewControllerMock.keyboardAnimationIsShowing;
345 XCTAssertTrue(isShowingAnimation1);
346
347 // Start compounding show keyboard animation.
348 CGRect compoundingShowKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250);
349 CGRect compoundingShowKeyboardEndFrame = CGRectMake(0, screenHeight - 500, screenWidth, 500);
350 fakeNotification = [NSNotification
351 notificationWithName:UIKeyboardWillChangeFrameNotification
352 object:nil
353 userInfo:@{
354 @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingShowKeyboardBeginFrame),
355 @"UIKeyboardFrameEndUserInfoKey" : @(compoundingShowKeyboardEndFrame),
356 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
357 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
358 }];
359
360 [viewControllerMock handleKeyboardNotification:fakeNotification];
361 BOOL isShowingAnimation2 = viewControllerMock.keyboardAnimationIsShowing;
362 XCTAssertTrue(isShowingAnimation2);
363 XCTAssertTrue(isShowingAnimation1 == isShowingAnimation2);
364
365 // Start hide keyboard animation.
366 CGRect initialHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 500, screenWidth, 250);
367 CGRect initialHideKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500);
368 fakeNotification = [NSNotification
369 notificationWithName:UIKeyboardWillChangeFrameNotification
370 object:nil
371 userInfo:@{
372 @"UIKeyboardFrameBeginUserInfoKey" : @(initialHideKeyboardBeginFrame),
373 @"UIKeyboardFrameEndUserInfoKey" : @(initialHideKeyboardEndFrame),
374 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
375 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
376 }];
377
378 [viewControllerMock handleKeyboardNotification:fakeNotification];
379 BOOL isShowingAnimation3 = viewControllerMock.keyboardAnimationIsShowing;
380 XCTAssertFalse(isShowingAnimation3);
381 XCTAssertTrue(isShowingAnimation2 != isShowingAnimation3);
382
383 // Start compounding hide keyboard animation.
384 CGRect compoundingHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250);
385 CGRect compoundingHideKeyboardEndFrame = CGRectMake(0, screenHeight, screenWidth, 500);
386 fakeNotification = [NSNotification
387 notificationWithName:UIKeyboardWillChangeFrameNotification
388 object:nil
389 userInfo:@{
390 @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingHideKeyboardBeginFrame),
391 @"UIKeyboardFrameEndUserInfoKey" : @(compoundingHideKeyboardEndFrame),
392 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
393 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
394 }];
395
396 [viewControllerMock handleKeyboardNotification:fakeNotification];
397 BOOL isShowingAnimation4 = viewControllerMock.keyboardAnimationIsShowing;
398 XCTAssertFalse(isShowingAnimation4);
399 XCTAssertTrue(isShowingAnimation3 == isShowingAnimation4);
400}
401
402- (void)testShouldIgnoreKeyboardNotification {
403 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
404 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
405 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
406 nibName:nil
407 bundle:nil];
408 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
409 UIScreen* screen = [self setUpMockScreen];
410 CGRect viewFrame = screen.bounds;
411 [self setUpMockView:viewControllerMock
412 screen:screen
413 viewFrame:viewFrame
414 convertedFrame:viewFrame];
415
416 CGFloat screenWidth = screen.bounds.size.width;
417 CGFloat screenHeight = screen.bounds.size.height;
418 CGRect emptyKeyboard = CGRectZero;
419 CGRect zeroHeightKeyboard = CGRectMake(0, 0, screenWidth, 0);
420 CGRect validKeyboardEndFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320);
421 BOOL isLocal = NO;
422
423 // Hide notification, valid keyboard
424 NSNotification* notification =
425 [NSNotification notificationWithName:UIKeyboardWillHideNotification
426 object:nil
427 userInfo:@{
428 @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame),
429 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
430 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
431 }];
432
433 BOOL shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
434 XCTAssertTrue(shouldIgnore == NO);
435
436 // All zero keyboard
437 isLocal = YES;
438 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
439 object:nil
440 userInfo:@{
441 @"UIKeyboardFrameEndUserInfoKey" : @(emptyKeyboard),
442 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
443 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
444 }];
445 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
446 XCTAssertTrue(shouldIgnore == YES);
447
448 // Zero height keyboard
449 isLocal = NO;
450 notification =
451 [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
452 object:nil
453 userInfo:@{
454 @"UIKeyboardFrameEndUserInfoKey" : @(zeroHeightKeyboard),
455 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
456 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
457 }];
458 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
459 XCTAssertTrue(shouldIgnore == NO);
460
461 // Valid keyboard, triggered from another app
462 isLocal = NO;
463 notification =
464 [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
465 object:nil
466 userInfo:@{
467 @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame),
468 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
469 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
470 }];
471 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
472 XCTAssertTrue(shouldIgnore == YES);
473
474 // Valid keyboard
475 isLocal = YES;
476 notification =
477 [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
478 object:nil
479 userInfo:@{
480 @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame),
481 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
482 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
483 }];
484 shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification];
485 XCTAssertTrue(shouldIgnore == NO);
486}
487
488- (void)testKeyboardAnimationWillNotCrashWhenEngineDestroyed {
489 FlutterEngine* engine = [[FlutterEngine alloc] init];
490 [engine runWithEntrypoint:nil];
491 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
492 nibName:nil
493 bundle:nil];
494 [viewController setUpKeyboardAnimationVsyncClient:^(fml::TimePoint){
495 }];
496 [engine destroyContext];
497}
498
499- (void)testKeyboardAnimationWillWaitUIThreadVsync {
500 // We need to make sure the new viewport metrics get sent after the
501 // begin frame event has processed. And this test is to expect that the callback
502 // will sync with UI thread. So just simulate a lot of works on UI thread and
503 // test the keyboard animation callback will execute until UI task completed.
504 // Related issue: https://github.com/flutter/flutter/issues/120555.
505
506 FlutterEngine* engine = [[FlutterEngine alloc] init];
507 [engine runWithEntrypoint:nil];
508 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
509 nibName:nil
510 bundle:nil];
511 // Post a task to UI thread to block the thread.
512 const int delayTime = 1;
513 [engine uiTaskRunner]->PostTask([] { sleep(delayTime); });
514 XCTestExpectation* expectation = [self expectationWithDescription:@"keyboard animation callback"];
515
516 __block CFTimeInterval fulfillTime;
518 fulfillTime = CACurrentMediaTime();
519 [expectation fulfill];
520 };
521 CFTimeInterval startTime = CACurrentMediaTime();
522 [viewController setUpKeyboardAnimationVsyncClient:callback];
523 [self waitForExpectationsWithTimeout:5.0 handler:nil];
524 XCTAssertTrue(fulfillTime - startTime > delayTime);
525}
526
527- (void)testCalculateKeyboardAttachMode {
528 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
529 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
530 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
531 nibName:nil
532 bundle:nil];
533
534 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
535 UIScreen* screen = [self setUpMockScreen];
536 CGRect viewFrame = screen.bounds;
537 [self setUpMockView:viewControllerMock
538 screen:screen
539 viewFrame:viewFrame
540 convertedFrame:viewFrame];
541
542 CGFloat screenWidth = screen.bounds.size.width;
543 CGFloat screenHeight = screen.bounds.size.height;
544
545 // hide notification
546 CGRect keyboardFrame = CGRectZero;
547 NSNotification* notification =
548 [NSNotification notificationWithName:UIKeyboardWillHideNotification
549 object:nil
550 userInfo:@{
551 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
552 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
553 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
554 }];
555 FlutterKeyboardMode keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
556 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
557
558 // all zeros
559 keyboardFrame = CGRectZero;
560 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
561 object:nil
562 userInfo:@{
563 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
564 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
565 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
566 }];
567 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
568 XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating);
569
570 // 0 height
571 keyboardFrame = CGRectMake(0, 0, screenWidth, 0);
572 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
573 object:nil
574 userInfo:@{
575 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
576 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
577 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
578 }];
579 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
580 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
581
582 // floating
583 keyboardFrame = CGRectMake(0, 0, 320, 320);
584 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
585 object:nil
586 userInfo:@{
587 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
588 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
589 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
590 }];
591 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
592 XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating);
593
594 // undocked
595 keyboardFrame = CGRectMake(0, 0, screenWidth, 320);
596 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
597 object:nil
598 userInfo:@{
599 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
600 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
601 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
602 }];
603 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
604 XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating);
605
606 // docked
607 keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320);
608 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
609 object:nil
610 userInfo:@{
611 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
612 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
613 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
614 }];
615 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
616 XCTAssertTrue(keyboardMode == FlutterKeyboardModeDocked);
617
618 // docked - rounded values
619 CGFloat longDecimalHeight = 320.666666666666666;
620 keyboardFrame = CGRectMake(0, screenHeight - longDecimalHeight, screenWidth, longDecimalHeight);
621 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
622 object:nil
623 userInfo:@{
624 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
625 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
626 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
627 }];
628 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
629 XCTAssertTrue(keyboardMode == FlutterKeyboardModeDocked);
630
631 // hidden - rounded values
632 keyboardFrame = CGRectMake(0, screenHeight - .0000001, screenWidth, longDecimalHeight);
633 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
634 object:nil
635 userInfo:@{
636 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
637 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
638 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
639 }];
640 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
641 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
642
643 // hidden
644 keyboardFrame = CGRectMake(0, screenHeight, screenWidth, 320);
645 notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification
646 object:nil
647 userInfo:@{
648 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
649 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
650 @"UIKeyboardIsLocalUserInfoKey" : @(YES)
651 }];
652 keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification];
653 XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden);
654}
655
656- (void)testCalculateMultitaskingAdjustment {
657 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
658 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
659 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
660 nibName:nil
661 bundle:nil];
662 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
663
664 UIScreen* screen = [self setUpMockScreen];
665 CGFloat screenWidth = screen.bounds.size.width;
666 CGFloat screenHeight = screen.bounds.size.height;
667 CGRect screenRect = screen.bounds;
668 CGRect viewOrigFrame = CGRectMake(0, 0, 320, screenHeight - 40);
669 CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40);
670 CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300);
671 id mockView = [self setUpMockView:viewControllerMock
672 screen:screen
673 viewFrame:viewOrigFrame
674 convertedFrame:convertedViewFrame];
675 id mockTraitCollection = OCMClassMock([UITraitCollection class]);
676 OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad);
677 OCMStub([mockTraitCollection horizontalSizeClass]).andReturn(UIUserInterfaceSizeClassCompact);
678 OCMStub([mockTraitCollection verticalSizeClass]).andReturn(UIUserInterfaceSizeClassRegular);
679 OCMStub([mockView traitCollection]).andReturn(mockTraitCollection);
680
681 CGFloat adjustment = [viewControllerMock calculateMultitaskingAdjustment:screenRect
682 keyboardFrame:keyboardFrame];
683 XCTAssertTrue(adjustment == 20);
684}
685
686- (void)testCalculateKeyboardInset {
687 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
688 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
689 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
690 nibName:nil
691 bundle:nil];
692 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
693 UIScreen* screen = [self setUpMockScreen];
694 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
695
696 CGFloat screenWidth = screen.bounds.size.width;
697 CGFloat screenHeight = screen.bounds.size.height;
698 CGRect viewOrigFrame = CGRectMake(0, 0, 320, screenHeight - 40);
699 CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40);
700 CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300);
701
702 [self setUpMockView:viewControllerMock
703 screen:screen
704 viewFrame:viewOrigFrame
705 convertedFrame:convertedViewFrame];
706
707 CGFloat inset = [viewControllerMock calculateKeyboardInset:keyboardFrame
708 keyboardMode:FlutterKeyboardModeDocked];
709 XCTAssertTrue(inset == 300 * screen.scale);
710}
711
712- (void)testHandleKeyboardNotification {
713 FlutterEngine* engine = [[FlutterEngine alloc] init];
714 [engine runWithEntrypoint:nil];
715 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
716 nibName:nil
717 bundle:nil];
718 // keyboard is empty
719 UIScreen* screen = [self setUpMockScreen];
720 CGFloat screenWidth = screen.bounds.size.width;
721 CGFloat screenHeight = screen.bounds.size.height;
722 CGRect keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320);
723 CGRect viewFrame = screen.bounds;
724 BOOL isLocal = YES;
725 NSNotification* notification =
726 [NSNotification notificationWithName:UIKeyboardWillShowNotification
727 object:nil
728 userInfo:@{
729 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
730 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
731 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
732 }];
733 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
734 [self setUpMockView:viewControllerMock
735 screen:screen
736 viewFrame:viewFrame
737 convertedFrame:viewFrame];
738 viewControllerMock.targetViewInsetBottom = 0;
739 XCTestExpectation* expectation = [self expectationWithDescription:@"update viewport"];
740 OCMStub([viewControllerMock updateViewportMetricsIfNeeded]).andDo(^(NSInvocation* invocation) {
741 [expectation fulfill];
742 });
743
744 [viewControllerMock handleKeyboardNotification:notification];
745 XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 320 * screen.scale);
746 OCMVerify([viewControllerMock startKeyBoardAnimation:0.25]);
747 [self waitForExpectationsWithTimeout:5.0 handler:nil];
748}
749
750- (void)testEnsureBottomInsetIsZeroWhenKeyboardDismissed {
751 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
752 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
753 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
754 nibName:nil
755 bundle:nil];
756
757 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
758 CGRect keyboardFrame = CGRectZero;
759 BOOL isLocal = YES;
760 NSNotification* fakeNotification =
761 [NSNotification notificationWithName:UIKeyboardWillHideNotification
762 object:nil
763 userInfo:@{
764 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
765 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25),
766 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
767 }];
768
769 viewControllerMock.targetViewInsetBottom = 10;
770 [viewControllerMock handleKeyboardNotification:fakeNotification];
771 XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 0);
772}
773
774- (void)testStopKeyBoardAnimationWhenReceivedWillHideNotificationAfterWillShowNotification {
775 // see: https://github.com/flutter/flutter/issues/112281
776
777 FlutterEngine* engine = [[FlutterEngine alloc] init];
778 [engine runWithEntrypoint:nil];
779 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
780 nibName:nil
781 bundle:nil];
782 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
783 UIScreen* screen = [self setUpMockScreen];
784 CGRect viewFrame = screen.bounds;
785 [self setUpMockView:viewControllerMock
786 screen:screen
787 viewFrame:viewFrame
788 convertedFrame:viewFrame];
789 viewControllerMock.targetViewInsetBottom = 0;
790
791 CGFloat screenHeight = screen.bounds.size.height;
792 CGFloat screenWidth = screen.bounds.size.height;
793 CGRect keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320);
794 BOOL isLocal = YES;
795
796 // Receive will show notification
797 NSNotification* fakeShowNotification =
798 [NSNotification notificationWithName:UIKeyboardWillShowNotification
799 object:nil
800 userInfo:@{
801 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
802 @"UIKeyboardAnimationDurationUserInfoKey" : @0.25,
803 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
804 }];
805 [viewControllerMock handleKeyboardNotification:fakeShowNotification];
806 XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 320 * screen.scale);
807
808 // Receive will hide notification
809 NSNotification* fakeHideNotification =
810 [NSNotification notificationWithName:UIKeyboardWillHideNotification
811 object:nil
812 userInfo:@{
813 @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame),
814 @"UIKeyboardAnimationDurationUserInfoKey" : @(0.0),
815 @"UIKeyboardIsLocalUserInfoKey" : @(isLocal)
816 }];
817 [viewControllerMock handleKeyboardNotification:fakeHideNotification];
818 XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 0);
819
820 // Check if the keyboard animation is stopped.
821 XCTAssertNil(viewControllerMock.keyboardAnimationView);
822 XCTAssertNil(viewControllerMock.keyboardSpringAnimation);
823}
824
825- (void)testEnsureViewportMetricsWillInvokeAndDisplayLinkWillInvalidateInViewDidDisappear {
826 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
827 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
828 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
829 nibName:nil
830 bundle:nil];
831 id viewControllerMock = OCMPartialMock(viewController);
832 [viewControllerMock viewDidDisappear:YES];
833 OCMVerify([viewControllerMock ensureViewportMetricsIsCorrect]);
834 OCMVerify([viewControllerMock invalidateKeyboardAnimationVSyncClient]);
835}
836
837- (void)testViewDidDisappearDoesntPauseEngineWhenNotTheViewController {
838 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
839 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
840 mockEngine.lifecycleChannel = lifecycleChannel;
841 FlutterViewController* viewControllerA =
842 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
843 FlutterViewController* viewControllerB =
844 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
845 id viewControllerMock = OCMPartialMock(viewControllerA);
846 OCMStub([viewControllerMock surfaceUpdated:NO]);
847 mockEngine.viewController = viewControllerB;
848 [viewControllerA viewDidDisappear:NO];
849 OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]);
850 OCMReject([viewControllerMock surfaceUpdated:[OCMArg any]]);
851}
852
853- (void)testAppWillTerminateViewDidDestroyTheEngine {
854 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
855 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
856 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
857 nibName:nil
858 bundle:nil];
859 id viewControllerMock = OCMPartialMock(viewController);
860 OCMStub([viewControllerMock goToApplicationLifecycle:@"AppLifecycleState.detached"]);
861 OCMStub([mockEngine destroyContext]);
862 [viewController applicationWillTerminate:nil];
863 OCMVerify([viewControllerMock goToApplicationLifecycle:@"AppLifecycleState.detached"]);
864 OCMVerify([mockEngine destroyContext]);
865}
866
867- (void)testViewDidDisappearDoesPauseEngineWhenIsTheViewController {
868 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
869 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
870 mockEngine.lifecycleChannel = lifecycleChannel;
871 __weak FlutterViewController* weakViewController;
872 @autoreleasepool {
873 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
874 nibName:nil
875 bundle:nil];
876 weakViewController = viewController;
877 id viewControllerMock = OCMPartialMock(viewController);
878 OCMStub([viewControllerMock surfaceUpdated:NO]);
879 [viewController viewDidDisappear:NO];
880 OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]);
881 OCMVerify([viewControllerMock surfaceUpdated:NO]);
882 }
883 XCTAssertNil(weakViewController);
884}
885
886- (void)
887 testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillAppear {
888 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
889 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
890 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
891 nibName:nil
892 bundle:nil];
893 [viewController viewWillAppear:YES];
894 OCMVerify([viewController onUserSettingsChanged:nil]);
895}
896
897- (void)
898 testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillAppear {
899 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
900 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
901 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
902 nibName:nil
903 bundle:nil];
904 mockEngine.viewController = nil;
905 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
906 nibName:nil
907 bundle:nil];
908 mockEngine.viewController = nil;
909 mockEngine.viewController = viewControllerB;
910 [viewControllerA viewWillAppear:YES];
911 OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]);
912}
913
914- (void)
915 testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewDidAppear {
916 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
917 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
918 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
919 nibName:nil
920 bundle:nil];
921 [viewController viewDidAppear:YES];
922 OCMVerify([viewController onUserSettingsChanged:nil]);
923}
924
925- (void)
926 testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewDidAppear {
927 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
928 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
929 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
930 nibName:nil
931 bundle:nil];
932 mockEngine.viewController = nil;
933 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
934 nibName:nil
935 bundle:nil];
936 mockEngine.viewController = nil;
937 mockEngine.viewController = viewControllerB;
938 [viewControllerA viewDidAppear:YES];
939 OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]);
940}
941
942- (void)
943 testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillDisappear {
944 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
945 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
946 mockEngine.lifecycleChannel = lifecycleChannel;
947 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
948 nibName:nil
949 bundle:nil];
950 mockEngine.viewController = viewController;
951 [viewController viewWillDisappear:NO];
952 OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]);
953}
954
955- (void)
956 testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillDisappear {
957 id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]);
958 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
959 mockEngine.lifecycleChannel = lifecycleChannel;
960 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
961 nibName:nil
962 bundle:nil];
963 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
964 nibName:nil
965 bundle:nil];
966 mockEngine.viewController = viewControllerB;
967 [viewControllerA viewDidDisappear:NO];
968 OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]);
969}
970
971- (void)testUpdateViewportMetricsIfNeeded_DoesntInvokeEngineWhenNotTheViewController {
972 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
973 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
974 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
975 nibName:nil
976 bundle:nil];
977 mockEngine.viewController = nil;
978 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
979 nibName:nil
980 bundle:nil];
981 mockEngine.viewController = viewControllerB;
982 [viewControllerA updateViewportMetricsIfNeeded];
983 flutter::ViewportMetrics viewportMetrics;
984 OCMVerify(never(), [mockEngine updateViewportMetrics:viewportMetrics]);
985}
986
987- (void)testUpdateViewportMetricsIfNeeded_DoesInvokeEngineWhenIsTheViewController {
988 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
989 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
990 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
991 nibName:nil
992 bundle:nil];
993 mockEngine.viewController = viewController;
994 flutter::ViewportMetrics viewportMetrics;
995 OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs();
996 [viewController updateViewportMetricsIfNeeded];
997 OCMVerifyAll(mockEngine);
998}
999
1000- (void)testUpdateViewportMetricsIfNeeded_DoesNotInvokeEngineWhenShouldBeIgnoredDuringRotation {
1001 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1002 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1003 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1004 nibName:nil
1005 bundle:nil];
1006 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
1007 UIScreen* screen = [self setUpMockScreen];
1008 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
1009 mockEngine.viewController = viewController;
1010
1011 id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator));
1012 OCMStub([mockCoordinator transitionDuration]).andReturn(0.5);
1013
1014 // Mimic the device rotation.
1015 [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator];
1016 // Should not trigger the engine call when during rotation.
1017 [viewController updateViewportMetricsIfNeeded];
1018
1019 OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]);
1020}
1021
1022- (void)testViewWillTransitionToSize_DoesDelayEngineCallIfNonZeroDuration {
1023 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1024 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1025 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1026 nibName:nil
1027 bundle:nil];
1028 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
1029 UIScreen* screen = [self setUpMockScreen];
1030 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
1031 mockEngine.viewController = viewController;
1032
1033 // Mimic the device rotation with non-zero transition duration.
1034 NSTimeInterval transitionDuration = 0.5;
1035 id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator));
1036 OCMStub([mockCoordinator transitionDuration]).andReturn(transitionDuration);
1037
1038 flutter::ViewportMetrics viewportMetrics;
1039 OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs();
1040
1041 [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator];
1042 // Should not immediately call the engine (this request should be ignored).
1043 [viewController updateViewportMetricsIfNeeded];
1044 OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]);
1045
1046 // Should delay the engine call for half of the transition duration.
1047 // Wait for additional transitionDuration to allow updateViewportMetrics calls if any.
1048 XCTWaiterResult result = [XCTWaiter
1049 waitForExpectations:@[ [self expectationWithDescription:@"Waiting for rotation duration"] ]
1050 timeout:transitionDuration];
1051 XCTAssertEqual(result, XCTWaiterResultTimedOut);
1052
1053 OCMVerifyAll(mockEngine);
1054}
1055
1056- (void)testViewWillTransitionToSize_DoesNotDelayEngineCallIfZeroDuration {
1057 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1058 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1059 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1060 nibName:nil
1061 bundle:nil];
1062 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
1063 UIScreen* screen = [self setUpMockScreen];
1064 OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen);
1065 mockEngine.viewController = viewController;
1066
1067 // Mimic the device rotation with zero transition duration.
1068 id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator));
1069 OCMStub([mockCoordinator transitionDuration]).andReturn(0);
1070
1071 flutter::ViewportMetrics viewportMetrics;
1072 OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs();
1073
1074 // Should immediately trigger the engine call, without delay.
1075 [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator];
1076 [viewController updateViewportMetricsIfNeeded];
1077
1078 OCMVerifyAll(mockEngine);
1079}
1080
1081- (void)testViewDidLoadDoesntInvokeEngineWhenNotTheViewController {
1082 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1083 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1084 FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine
1085 nibName:nil
1086 bundle:nil];
1087 mockEngine.viewController = nil;
1088 FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine
1089 nibName:nil
1090 bundle:nil];
1091 mockEngine.viewController = viewControllerB;
1092 UIView* view = viewControllerA.view;
1093 XCTAssertNotNil(view);
1094 OCMVerify(never(), [mockEngine attachView]);
1095}
1096
1097- (void)testViewDidLoadDoesInvokeEngineWhenIsTheViewController {
1098 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1099 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1100 mockEngine.viewController = nil;
1101 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1102 nibName:nil
1103 bundle:nil];
1104 mockEngine.viewController = viewController;
1105 UIView* view = viewController.view;
1106 XCTAssertNotNil(view);
1107 OCMVerify(times(1), [mockEngine attachView]);
1108}
1109
1110- (void)testViewDidLoadDoesntInvokeEngineAttachViewWhenEngineNeedsLaunch {
1111 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1112 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1113 mockEngine.viewController = nil;
1114 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1115 nibName:nil
1116 bundle:nil];
1117 // sharedSetupWithProject sets the engine needs to be launched.
1118 [viewController sharedSetupWithProject:nil initialRoute:nil];
1119 mockEngine.viewController = viewController;
1120 UIView* view = viewController.view;
1121 XCTAssertNotNil(view);
1122 OCMVerify(never(), [mockEngine attachView]);
1123}
1124
1125- (void)testSplashScreenViewRemoveNotCrash {
1126 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:nil];
1127 [engine runWithEntrypoint:nil];
1128 FlutterViewController* flutterViewController =
1129 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1130 [flutterViewController setSplashScreenView:[[UIView alloc] init]];
1131 [flutterViewController setSplashScreenView:nil];
1132}
1133
1134- (void)testInternalPluginsWeakPtrNotCrash {
1135 FlutterSendKeyEvent sendEvent;
1136 @autoreleasepool {
1137 FlutterViewController* vc = [[FlutterViewController alloc] initWithProject:nil
1138 nibName:nil
1139 bundle:nil];
1140 [vc addInternalPlugins];
1141 FlutterKeyboardManager* keyboardManager = vc.keyboardManager;
1143 [(NSArray<id<FlutterKeyPrimaryResponder>>*)keyboardManager.primaryResponders firstObject];
1144 sendEvent = [keyPrimaryResponder sendEvent];
1145 }
1146
1147 if (sendEvent) {
1148 sendEvent({}, nil, nil);
1149 }
1150}
1151
1152// Regression test for https://github.com/flutter/engine/pull/32098.
1153- (void)testInternalPluginsInvokeInViewDidLoad {
1154 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1155 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1156 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1157 nibName:nil
1158 bundle:nil];
1159 UIView* view = viewController.view;
1160 // The implementation in viewDidLoad requires the viewControllers.viewLoaded is true.
1161 // Accessing the view to make sure the view loads in the memory,
1162 // which makes viewControllers.viewLoaded true.
1163 XCTAssertNotNil(view);
1164 [viewController viewDidLoad];
1165 OCMVerify([viewController addInternalPlugins]);
1166}
1167
1168- (void)testBinaryMessenger {
1169 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1170 nibName:nil
1171 bundle:nil];
1172 XCTAssertNotNil(vc);
1173 id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger));
1174 OCMStub([self.mockEngine binaryMessenger]).andReturn(messenger);
1175 XCTAssertEqual(vc.binaryMessenger, messenger);
1176 OCMVerify([self.mockEngine binaryMessenger]);
1177}
1178
1179- (void)testViewControllerIsReleased {
1180 __weak FlutterViewController* weakViewController;
1181 __weak UIView* weakView;
1182 @autoreleasepool {
1183 FlutterEngine* engine = [[FlutterEngine alloc] init];
1184
1185 [engine runWithEntrypoint:nil];
1186 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
1187 nibName:nil
1188 bundle:nil];
1189 weakViewController = viewController;
1190 [viewController loadView];
1191 [viewController viewDidLoad];
1192 weakView = viewController.view;
1193 XCTAssertTrue([viewController.view isKindOfClass:[FlutterView class]]);
1194 }
1195 XCTAssertNil(weakViewController);
1196 XCTAssertNil(weakView);
1197}
1198
1199#pragma mark - Platform Brightness
1200
1201- (void)testItReportsLightPlatformBrightnessByDefault {
1202 // Setup test.
1203 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1204 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1205
1206 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1207 nibName:nil
1208 bundle:nil];
1209
1210 // Exercise behavior under test.
1211 [vc traitCollectionDidChange:nil];
1212
1213 // Verify behavior.
1214 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1215 return [message[@"platformBrightness"] isEqualToString:@"light"];
1216 }]]);
1217
1218 // Clean up mocks
1219 [settingsChannel stopMocking];
1220}
1221
1222- (void)testItReportsPlatformBrightnessWhenViewWillAppear {
1223 // Setup test.
1224 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1225 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1226 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1227 OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
1228 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1229 nibName:nil
1230 bundle:nil];
1231
1232 // Exercise behavior under test.
1233 [vc viewWillAppear:false];
1234
1235 // Verify behavior.
1236 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1237 return [message[@"platformBrightness"] isEqualToString:@"light"];
1238 }]]);
1239
1240 // Clean up mocks
1241 [settingsChannel stopMocking];
1242}
1243
1244- (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt {
1245 // Setup test.
1246 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1247 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1248 id mockTraitCollection =
1249 [self fakeTraitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
1250
1251 // We partially mock the real FlutterViewController to act as the OS and report
1252 // the UITraitCollection of our choice. Mocking the object under test is not
1253 // desirable, but given that the OS does not offer a DI approach to providing
1254 // our own UITraitCollection, this seems to be the least bad option.
1255 id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine
1256 nibName:nil
1257 bundle:nil]);
1258 OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
1259
1260 // Exercise behavior under test.
1261 [partialMockVC traitCollectionDidChange:nil];
1262
1263 // Verify behavior.
1264 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1265 return [message[@"platformBrightness"] isEqualToString:@"dark"];
1266 }]]);
1267
1268 // Clean up mocks
1269 [partialMockVC stopMocking];
1270 [settingsChannel stopMocking];
1271 [mockTraitCollection stopMocking];
1272}
1273
1274// Creates a mocked UITraitCollection with nil values for everything except userInterfaceStyle,
1275// which is set to the given "style".
1276- (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style {
1277 id mockTraitCollection = OCMClassMock([UITraitCollection class]);
1278 OCMStub([mockTraitCollection userInterfaceStyle]).andReturn(style);
1279 return mockTraitCollection;
1280}
1281
1282#pragma mark - Platform Contrast
1283
1284- (void)testItReportsNormalPlatformContrastByDefault {
1285 // Setup test.
1286 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1287 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1288
1289 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1290 nibName:nil
1291 bundle:nil];
1292
1293 // Exercise behavior under test.
1294 [vc traitCollectionDidChange:nil];
1295
1296 // Verify behavior.
1297 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1298 return [message[@"platformContrast"] isEqualToString:@"normal"];
1299 }]]);
1300
1301 // Clean up mocks
1302 [settingsChannel stopMocking];
1303}
1304
1305- (void)testItReportsPlatformContrastWhenViewWillAppear {
1306 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1307 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1308
1309 // Setup test.
1310 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1311 OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
1312 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1313 nibName:nil
1314 bundle:nil];
1315
1316 // Exercise behavior under test.
1317 [vc viewWillAppear:false];
1318
1319 // Verify behavior.
1320 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1321 return [message[@"platformContrast"] isEqualToString:@"normal"];
1322 }]]);
1323
1324 // Clean up mocks
1325 [settingsChannel stopMocking];
1326}
1327
1328- (void)testItReportsHighContrastWhenTraitCollectionRequestsIt {
1329 // Setup test.
1330 id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1331 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1332
1333 id mockTraitCollection = [self fakeTraitCollectionWithContrast:UIAccessibilityContrastHigh];
1334
1335 // We partially mock the real FlutterViewController to act as the OS and report
1336 // the UITraitCollection of our choice. Mocking the object under test is not
1337 // desirable, but given that the OS does not offer a DI approach to providing
1338 // our own UITraitCollection, this seems to be the least bad option.
1339 id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine
1340 nibName:nil
1341 bundle:nil]);
1342 OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
1343
1344 // Exercise behavior under test.
1345 [partialMockVC traitCollectionDidChange:mockTraitCollection];
1346
1347 // Verify behavior.
1348 OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1349 return [message[@"platformContrast"] isEqualToString:@"high"];
1350 }]]);
1351
1352 // Clean up mocks
1353 [partialMockVC stopMocking];
1354 [settingsChannel stopMocking];
1355 [mockTraitCollection stopMocking];
1356}
1357
1358- (void)testItReportsAlwaysUsed24HourFormat {
1359 // Setup test.
1360 id settingsChannel = OCMStrictClassMock([FlutterBasicMessageChannel class]);
1361 OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1362 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1363 nibName:nil
1364 bundle:nil];
1365 // Test the YES case.
1366 id mockHourFormat = OCMClassMock([FlutterHourFormat class]);
1367 OCMStub([mockHourFormat isAlwaysUse24HourFormat]).andReturn(YES);
1368 OCMExpect([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1369 return [message[@"alwaysUse24HourFormat"] isEqual:@(YES)];
1370 }]]);
1371 [vc onUserSettingsChanged:nil];
1372 [mockHourFormat stopMocking];
1373
1374 // Test the NO case.
1375 mockHourFormat = OCMClassMock([FlutterHourFormat class]);
1376 OCMStub([mockHourFormat isAlwaysUse24HourFormat]).andReturn(NO);
1377 OCMExpect([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1378 return [message[@"alwaysUse24HourFormat"] isEqual:@(NO)];
1379 }]]);
1380 [vc onUserSettingsChanged:nil];
1381 [mockHourFormat stopMocking];
1382
1383 // Clean up mocks.
1384 [settingsChannel stopMocking];
1385}
1386
1387- (void)testItReportsAccessibilityOnOffSwitchLabelsFlagNotSet {
1388 // Setup test.
1390 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
1391 id partialMockViewController = OCMPartialMock(viewController);
1392 OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(NO);
1393
1394 // Exercise behavior under test.
1395 int32_t flags = [partialMockViewController accessibilityFlags];
1396
1397 // Verify behavior.
1398 XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) == 0);
1399}
1400
1401- (void)testItReportsAccessibilityOnOffSwitchLabelsFlagSet {
1402 // Setup test.
1404 [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
1405 id partialMockViewController = OCMPartialMock(viewController);
1406 OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(YES);
1407
1408 // Exercise behavior under test.
1409 int32_t flags = [partialMockViewController accessibilityFlags];
1410
1411 // Verify behavior.
1412 XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) != 0);
1413}
1414
1415- (void)testAccessibilityPerformEscapePopsRoute {
1416 FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1417 [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1418 id mockNavigationChannel = OCMClassMock([FlutterMethodChannel class]);
1419 OCMStub([mockEngine navigationChannel]).andReturn(mockNavigationChannel);
1420
1421 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1422 nibName:nil
1423 bundle:nil];
1424 XCTAssertTrue([viewController accessibilityPerformEscape]);
1425
1426 OCMVerify([mockNavigationChannel invokeMethod:@"popRoute" arguments:nil]);
1427
1428 [mockNavigationChannel stopMocking];
1429}
1430
1431- (void)testPerformOrientationUpdateForcesOrientationChange {
1432 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1433 currentOrientation:UIInterfaceOrientationLandscapeLeft
1434 didChangeOrientation:YES
1435 resultingOrientation:UIInterfaceOrientationPortrait];
1436
1437 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1438 currentOrientation:UIInterfaceOrientationLandscapeRight
1439 didChangeOrientation:YES
1440 resultingOrientation:UIInterfaceOrientationPortrait];
1441
1442 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1443 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1444 didChangeOrientation:YES
1445 resultingOrientation:UIInterfaceOrientationPortrait];
1446
1447 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1448 currentOrientation:UIInterfaceOrientationLandscapeLeft
1449 didChangeOrientation:YES
1450 resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1451
1452 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1453 currentOrientation:UIInterfaceOrientationLandscapeRight
1454 didChangeOrientation:YES
1455 resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1456
1457 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1458 currentOrientation:UIInterfaceOrientationPortrait
1459 didChangeOrientation:YES
1460 resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1461
1462 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1463 currentOrientation:UIInterfaceOrientationPortrait
1464 didChangeOrientation:YES
1465 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1466
1467 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1468 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1469 didChangeOrientation:YES
1470 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1471
1472 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1473 currentOrientation:UIInterfaceOrientationPortrait
1474 didChangeOrientation:YES
1475 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1476
1477 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1478 currentOrientation:UIInterfaceOrientationLandscapeRight
1479 didChangeOrientation:YES
1480 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1481
1482 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1483 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1484 didChangeOrientation:YES
1485 resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1486
1487 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1488 currentOrientation:UIInterfaceOrientationPortrait
1489 didChangeOrientation:YES
1490 resultingOrientation:UIInterfaceOrientationLandscapeRight];
1491
1492 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1493 currentOrientation:UIInterfaceOrientationLandscapeLeft
1494 didChangeOrientation:YES
1495 resultingOrientation:UIInterfaceOrientationLandscapeRight];
1496
1497 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1498 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1499 didChangeOrientation:YES
1500 resultingOrientation:UIInterfaceOrientationLandscapeRight];
1501
1502 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1503 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1504 didChangeOrientation:YES
1505 resultingOrientation:UIInterfaceOrientationPortrait];
1506}
1507
1508- (void)testPerformOrientationUpdateDoesNotForceOrientationChange {
1509 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1510 currentOrientation:UIInterfaceOrientationPortrait
1511 didChangeOrientation:NO
1512 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1513
1514 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1515 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1516 didChangeOrientation:NO
1517 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1518
1519 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1520 currentOrientation:UIInterfaceOrientationLandscapeLeft
1521 didChangeOrientation:NO
1522 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1523
1524 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1525 currentOrientation:UIInterfaceOrientationLandscapeRight
1526 didChangeOrientation:NO
1527 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1528
1529 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1530 currentOrientation:UIInterfaceOrientationPortrait
1531 didChangeOrientation:NO
1532 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1533
1534 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1535 currentOrientation:UIInterfaceOrientationLandscapeLeft
1536 didChangeOrientation:NO
1537 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1538
1539 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1540 currentOrientation:UIInterfaceOrientationLandscapeRight
1541 didChangeOrientation:NO
1542 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1543
1544 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1545 currentOrientation:UIInterfaceOrientationPortrait
1546 didChangeOrientation:NO
1547 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1548
1549 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1550 currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1551 didChangeOrientation:NO
1552 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1553
1554 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1555 currentOrientation:UIInterfaceOrientationLandscapeLeft
1556 didChangeOrientation:NO
1557 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1558
1559 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1560 currentOrientation:UIInterfaceOrientationLandscapeRight
1561 didChangeOrientation:NO
1562 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1563
1564 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1565 currentOrientation:UIInterfaceOrientationLandscapeLeft
1566 didChangeOrientation:NO
1567 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1568
1569 [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1570 currentOrientation:UIInterfaceOrientationLandscapeRight
1571 didChangeOrientation:NO
1572 resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1573}
1574
1575// Perform an orientation update test that fails when the expected outcome
1576// for an orientation update is not met
1577- (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask
1578 currentOrientation:(UIInterfaceOrientation)currentOrientation
1579 didChangeOrientation:(BOOL)didChange
1580 resultingOrientation:(UIInterfaceOrientation)resultingOrientation {
1581 id mockApplication = OCMClassMock([UIApplication class]);
1582 id mockWindowScene;
1583 id deviceMock;
1584 id mockVC;
1585 __block __weak id weakPreferences;
1586 @autoreleasepool {
1587 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1588 nibName:nil
1589 bundle:nil];
1590
1591 if (@available(iOS 16.0, *)) {
1592 mockWindowScene = OCMClassMock([UIWindowScene class]);
1593 mockVC = OCMPartialMock(realVC);
1594 OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1595 if (realVC.supportedInterfaceOrientations == mask) {
1596 OCMReject([mockWindowScene requestGeometryUpdateWithPreferences:[OCMArg any]
1597 errorHandler:[OCMArg any]]);
1598 } else {
1599 // iOS 16 will decide whether to rotate based on the new preference, so always set it
1600 // when it changes.
1601 OCMExpect([mockWindowScene
1602 requestGeometryUpdateWithPreferences:[OCMArg checkWithBlock:^BOOL(
1603 UIWindowSceneGeometryPreferencesIOS*
1604 preferences) {
1605 weakPreferences = preferences;
1606 return preferences.interfaceOrientations == mask;
1607 }]
1608 errorHandler:[OCMArg any]]);
1609 }
1610 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
1611 OCMStub([mockApplication connectedScenes]).andReturn([NSSet setWithObject:mockWindowScene]);
1612 } else {
1613 deviceMock = OCMPartialMock([UIDevice currentDevice]);
1614 if (!didChange) {
1615 OCMReject([deviceMock setValue:[OCMArg any] forKey:@"orientation"]);
1616 } else {
1617 OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]);
1618 }
1619 mockWindowScene = OCMClassMock([UIWindowScene class]);
1620 mockVC = OCMPartialMock(realVC);
1621 OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1622 OCMStub(((UIWindowScene*)mockWindowScene).interfaceOrientation).andReturn(currentOrientation);
1623 }
1624
1625 [realVC performOrientationUpdate:mask];
1626 if (@available(iOS 16.0, *)) {
1627 OCMVerifyAll(mockWindowScene);
1628 } else {
1629 OCMVerifyAll(deviceMock);
1630 }
1631 }
1632 [mockWindowScene stopMocking];
1633 [deviceMock stopMocking];
1634 [mockApplication stopMocking];
1635 XCTAssertNil(weakPreferences);
1636}
1637
1638// Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast,
1639// which is set to the given "contrast".
1640- (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast {
1641 id mockTraitCollection = OCMClassMock([UITraitCollection class]);
1642 OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast);
1643 return mockTraitCollection;
1644}
1645
1646- (void)testWillDeallocNotification {
1647 XCTestExpectation* expectation =
1648 [[XCTestExpectation alloc] initWithDescription:@"notification called"];
1649 id engine = [[MockEngine alloc] init];
1650 @autoreleasepool {
1651 // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
1652 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1653 nibName:nil
1654 bundle:nil];
1655 [NSNotificationCenter.defaultCenter addObserverForName:FlutterViewControllerWillDealloc
1656 object:nil
1657 queue:[NSOperationQueue mainQueue]
1658 usingBlock:^(NSNotification* _Nonnull note) {
1659 [expectation fulfill];
1660 }];
1661 XCTAssertNotNil(realVC);
1662 realVC = nil;
1663 }
1664 [self waitForExpectations:@[ expectation ] timeout:1.0];
1665}
1666
1667- (void)testReleasesKeyboardManagerOnDealloc {
1668 __weak FlutterKeyboardManager* weakKeyboardManager = nil;
1669 @autoreleasepool {
1671
1672 [viewController addInternalPlugins];
1673 weakKeyboardManager = viewController.keyboardManager;
1674 XCTAssertNotNil(weakKeyboardManager);
1675 [viewController deregisterNotifications];
1676 viewController = nil;
1677 }
1678 // View controller has released the keyboard manager.
1679 XCTAssertNil(weakKeyboardManager);
1680}
1681
1682- (void)testDoesntLoadViewInInit {
1683 FlutterDartProject* project = [[FlutterDartProject alloc] init];
1684 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1685 [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1686 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1687 nibName:nil
1688 bundle:nil];
1689 XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown");
1690 engine.viewController = nil;
1691}
1692
1693- (void)testHideOverlay {
1694 FlutterDartProject* project = [[FlutterDartProject alloc] init];
1695 FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1696 [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1697 FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1698 nibName:nil
1699 bundle:nil];
1700 XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @"");
1701 [NSNotificationCenter.defaultCenter postNotificationName:FlutterViewControllerHideHomeIndicator
1702 object:nil];
1703 XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @"");
1704 engine.viewController = nil;
1705}
1706
1707- (void)testNotifyLowMemory {
1708 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
1709 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1710 nibName:nil
1711 bundle:nil];
1712 id viewControllerMock = OCMPartialMock(viewController);
1713 OCMStub([viewControllerMock surfaceUpdated:NO]);
1714 [viewController beginAppearanceTransition:NO animated:NO];
1715 [viewController endAppearanceTransition];
1716 XCTAssertTrue(mockEngine.didCallNotifyLowMemory);
1717}
1718
1719- (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback {
1720 NSMutableDictionary* replyMessage = [@{
1721 @"handled" : @YES,
1722 } mutableCopy];
1723 // Response is async, so we have to post it to the run loop instead of calling
1724 // it directly.
1725 self.messageSent = message;
1726 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
1727 ^() {
1728 callback(replyMessage);
1729 });
1730}
1731
1732- (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {
1733 if (@available(iOS 13.4, *)) {
1734 // noop
1735 } else {
1736 return;
1737 }
1738 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
1739 mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1740 OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1741 .andCall(self, @selector(sendMessage:reply:));
1742 OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1743 mockEngine.textInputPlugin = self.mockTextInputPlugin;
1744
1745 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1746 nibName:nil
1747 bundle:nil];
1748
1749 // Allocate the keyboard manager in the view controller by adding the internal
1750 // plugins.
1751 [vc addInternalPlugins];
1752
1753 [vc handlePressEvent:keyUpEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0)
1754 nextAction:^(){
1755 }];
1756
1757 XCTAssert(self.messageSent != nil);
1758 XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1759 XCTAssert([self.messageSent[@"type"] isEqualToString:@"keyup"]);
1760 XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1761 XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1762 XCTAssert([self.messageSent[@"characters"] isEqualToString:@""]);
1763 XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@""]);
1764 [vc deregisterNotifications];
1765}
1766
1767- (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) {
1768 if (@available(iOS 13.4, *)) {
1769 // noop
1770 } else {
1771 return;
1772 }
1773
1774 FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init];
1775 mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1776 OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1777 .andCall(self, @selector(sendMessage:reply:));
1778 OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1779 mockEngine.textInputPlugin = self.mockTextInputPlugin;
1780
1781 __strong FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1782 nibName:nil
1783 bundle:nil];
1784 // Allocate the keyboard manager in the view controller by adding the internal
1785 // plugins.
1786 [vc addInternalPlugins];
1787
1788 [vc handlePressEvent:keyDownEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0f, "A",
1789 "a")
1790 nextAction:^(){
1791 }];
1792
1793 XCTAssert(self.messageSent != nil);
1794 XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1795 XCTAssert([self.messageSent[@"type"] isEqualToString:@"keydown"]);
1796 XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1797 XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1798 XCTAssert([self.messageSent[@"characters"] isEqualToString:@"A"]);
1799 XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@"a"]);
1800 [vc deregisterNotifications];
1801 vc = nil;
1802}
1803
1804- (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) {
1805 if (@available(iOS 13.4, *)) {
1806 // noop
1807 } else {
1808 return;
1809 }
1810 id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1811 OCMStub([keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1812 .andCall(self, @selector(sendMessage:reply:));
1813 OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1814 OCMStub([self.mockEngine keyEventChannel]).andReturn(keyEventChannel);
1815
1816 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1817 nibName:nil
1818 bundle:nil];
1819
1820 // Allocate the keyboard manager in the view controller by adding the internal
1821 // plugins.
1822 [vc addInternalPlugins];
1823
1824 [vc handlePressEvent:keyEventWithPhase(UIPressPhaseStationary, UIKeyboardHIDUsageKeyboardA,
1825 UIKeyModifierShift, 123.0)
1826 nextAction:^(){
1827 }];
1828 [vc handlePressEvent:keyEventWithPhase(UIPressPhaseCancelled, UIKeyboardHIDUsageKeyboardA,
1829 UIKeyModifierShift, 123.0)
1830 nextAction:^(){
1831 }];
1832 [vc handlePressEvent:keyEventWithPhase(UIPressPhaseChanged, UIKeyboardHIDUsageKeyboardA,
1833 UIKeyModifierShift, 123.0)
1834 nextAction:^(){
1835 }];
1836
1837 XCTAssert(self.messageSent == nil);
1838 OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]);
1839 [vc deregisterNotifications];
1840}
1841
1842- (void)testPanGestureRecognizer API_AVAILABLE(ios(13.4)) {
1843 if (@available(iOS 13.4, *)) {
1844 // noop
1845 } else {
1846 return;
1847 }
1848
1849 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1850 nibName:nil
1851 bundle:nil];
1852 XCTAssertNotNil(vc);
1853 UIView* view = vc.view;
1854 XCTAssertNotNil(view);
1855 NSArray* gestureRecognizers = view.gestureRecognizers;
1856 XCTAssertNotNil(gestureRecognizers);
1857
1858 BOOL found = NO;
1859 for (id gesture in gestureRecognizers) {
1860 if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
1861 found = YES;
1862 break;
1863 }
1864 }
1865 XCTAssertTrue(found);
1866}
1867
1868- (void)testMouseSupport API_AVAILABLE(ios(13.4)) {
1869 if (@available(iOS 13.4, *)) {
1870 // noop
1871 } else {
1872 return;
1873 }
1874
1875 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1876 nibName:nil
1877 bundle:nil];
1878 XCTAssertNotNil(vc);
1879
1880 id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]);
1881 XCTAssertNotNil(mockPanGestureRecognizer);
1882
1883 [vc discreteScrollEvent:mockPanGestureRecognizer];
1884
1885 // The mouse position within panGestureRecognizer should be checked
1886 [[mockPanGestureRecognizer verify] locationInView:[OCMArg any]];
1887 [[[self.mockEngine verify] ignoringNonObjectArgs]
1888 dispatchPointerDataPacket:std::make_unique<flutter::PointerDataPacket>(0)];
1889}
1890
1891- (void)testFakeEventTimeStamp {
1892 FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1893 nibName:nil
1894 bundle:nil];
1895 XCTAssertNotNil(vc);
1896
1897 flutter::PointerData pointer_data = [vc generatePointerDataForFake];
1898 int64_t current_micros = [[NSProcessInfo processInfo] systemUptime] * 1000 * 1000;
1899 int64_t interval_micros = current_micros - pointer_data.time_stamp;
1900 const int64_t tolerance_millis = 2;
1901 XCTAssertTrue(interval_micros / 1000 < tolerance_millis,
1902 @"PointerData.time_stamp should be equal to NSProcessInfo.systemUptime");
1903}
1904
1905- (void)testSplashScreenViewCanSetNil {
1906 FlutterViewController* flutterViewController =
1907 [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
1908 [flutterViewController setSplashScreenView:nil];
1909}
1910
1911- (void)testLifeCycleNotificationApplicationBecameActive {
1912 FlutterEngine* engine = [[FlutterEngine alloc] init];
1913 [engine runWithEntrypoint:nil];
1914 FlutterViewController* flutterViewController =
1915 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1916 UIWindow* window = [[UIWindow alloc] init];
1917 [window addSubview:flutterViewController.view];
1918 flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100);
1919 [flutterViewController viewDidLayoutSubviews];
1920 NSNotification* sceneNotification =
1921 [NSNotification notificationWithName:UISceneDidActivateNotification object:nil userInfo:nil];
1922 NSNotification* applicationNotification =
1923 [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
1924 object:nil
1925 userInfo:nil];
1926 id mockVC = OCMPartialMock(flutterViewController);
1927 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
1928 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
1929 OCMReject([mockVC sceneBecameActive:[OCMArg any]]);
1930 OCMVerify([mockVC applicationBecameActive:[OCMArg any]]);
1931 XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1932 OCMVerify([mockVC surfaceUpdated:YES]);
1933 XCTestExpectation* timeoutApplicationLifeCycle =
1934 [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
1935 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
1936 dispatch_get_main_queue(), ^{
1937 [timeoutApplicationLifeCycle fulfill];
1938 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
1939 [flutterViewController deregisterNotifications];
1940 });
1941 [self waitForExpectationsWithTimeout:5.0 handler:nil];
1942}
1943
1944- (void)testLifeCycleNotificationSceneBecameActive {
1945 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
1946 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
1947 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
1948 });
1949 FlutterEngine* engine = [[FlutterEngine alloc] init];
1950 [engine runWithEntrypoint:nil];
1951 FlutterViewController* flutterViewController =
1952 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1953 UIWindow* window = [[UIWindow alloc] init];
1954 [window addSubview:flutterViewController.view];
1955 flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100);
1956 [flutterViewController viewDidLayoutSubviews];
1957 NSNotification* sceneNotification =
1958 [NSNotification notificationWithName:UISceneDidActivateNotification object:nil userInfo:nil];
1959 NSNotification* applicationNotification =
1960 [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
1961 object:nil
1962 userInfo:nil];
1963 id mockVC = OCMPartialMock(flutterViewController);
1964 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
1965 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
1966 OCMVerify([mockVC sceneBecameActive:[OCMArg any]]);
1967 OCMReject([mockVC applicationBecameActive:[OCMArg any]]);
1968 XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1969 OCMVerify([mockVC surfaceUpdated:YES]);
1970 XCTestExpectation* timeoutApplicationLifeCycle =
1971 [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
1972 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
1973 dispatch_get_main_queue(), ^{
1974 [timeoutApplicationLifeCycle fulfill];
1975 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
1976 [flutterViewController deregisterNotifications];
1977 });
1978 [self waitForExpectationsWithTimeout:5.0 handler:nil];
1979 [mockBundle stopMocking];
1980}
1981
1982- (void)testLifeCycleNotificationApplicationWillResignActive {
1983 FlutterEngine* engine = [[FlutterEngine alloc] init];
1984 [engine runWithEntrypoint:nil];
1985 FlutterViewController* flutterViewController =
1986 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1987 NSNotification* sceneNotification =
1988 [NSNotification notificationWithName:UISceneWillDeactivateNotification
1989 object:nil
1990 userInfo:nil];
1991 NSNotification* applicationNotification =
1992 [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
1993 object:nil
1994 userInfo:nil];
1995 id mockVC = OCMPartialMock(flutterViewController);
1996 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
1997 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
1998 OCMReject([mockVC sceneWillResignActive:[OCMArg any]]);
1999 OCMVerify([mockVC applicationWillResignActive:[OCMArg any]]);
2000 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2001 [flutterViewController deregisterNotifications];
2002}
2003
2004- (void)testLifeCycleNotificationSceneWillResignActive {
2005 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
2006 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
2007 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
2008 });
2009 FlutterEngine* engine = [[FlutterEngine alloc] init];
2010 [engine runWithEntrypoint:nil];
2011 FlutterViewController* flutterViewController =
2012 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2013 NSNotification* sceneNotification =
2014 [NSNotification notificationWithName:UISceneWillDeactivateNotification
2015 object:nil
2016 userInfo:nil];
2017 NSNotification* applicationNotification =
2018 [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
2019 object:nil
2020 userInfo:nil];
2021 id mockVC = OCMPartialMock(flutterViewController);
2022 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
2023 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
2024 OCMVerify([mockVC sceneWillResignActive:[OCMArg any]]);
2025 OCMReject([mockVC applicationWillResignActive:[OCMArg any]]);
2026 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2027 [flutterViewController deregisterNotifications];
2028 [mockBundle stopMocking];
2029}
2030
2031- (void)testLifeCycleNotificationApplicationWillTerminate {
2032 FlutterEngine* engine = [[FlutterEngine alloc] init];
2033 [engine runWithEntrypoint:nil];
2034 FlutterViewController* flutterViewController =
2035 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2036 NSNotification* sceneNotification =
2037 [NSNotification notificationWithName:UISceneDidDisconnectNotification
2038 object:nil
2039 userInfo:nil];
2040 NSNotification* applicationNotification =
2041 [NSNotification notificationWithName:UIApplicationWillTerminateNotification
2042 object:nil
2043 userInfo:nil];
2044 id mockVC = OCMPartialMock(flutterViewController);
2045 id mockEngine = OCMPartialMock(engine);
2046 OCMStub([mockVC engine]).andReturn(mockEngine);
2047 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
2048 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
2049 OCMReject([mockVC sceneWillDisconnect:[OCMArg any]]);
2050 OCMVerify([mockVC applicationWillTerminate:[OCMArg any]]);
2051 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]);
2052 OCMVerify([mockEngine destroyContext]);
2053 [flutterViewController deregisterNotifications];
2054}
2055
2056- (void)testLifeCycleNotificationSceneWillTerminate {
2057 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
2058 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
2059 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
2060 });
2061 FlutterEngine* engine = [[FlutterEngine alloc] init];
2062 [engine runWithEntrypoint:nil];
2063 FlutterViewController* flutterViewController =
2064 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2065 NSNotification* sceneNotification =
2066 [NSNotification notificationWithName:UISceneDidDisconnectNotification
2067 object:nil
2068 userInfo:nil];
2069 NSNotification* applicationNotification =
2070 [NSNotification notificationWithName:UIApplicationWillTerminateNotification
2071 object:nil
2072 userInfo:nil];
2073 id mockVC = OCMPartialMock(flutterViewController);
2074 id mockEngine = OCMPartialMock(engine);
2075 OCMStub([mockVC engine]).andReturn(mockEngine);
2076 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
2077 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
2078 OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]);
2079 OCMReject([mockVC applicationWillTerminate:[OCMArg any]]);
2080 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]);
2081 OCMVerify([mockEngine destroyContext]);
2082 [flutterViewController deregisterNotifications];
2083 [mockBundle stopMocking];
2084}
2085
2086- (void)testLifeCycleNotificationApplicationDidEnterBackground {
2087 FlutterEngine* engine = [[FlutterEngine alloc] init];
2088 [engine runWithEntrypoint:nil];
2089 FlutterViewController* flutterViewController =
2090 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2091 NSNotification* sceneNotification =
2092 [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
2093 object:nil
2094 userInfo:nil];
2095 NSNotification* applicationNotification =
2096 [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
2097 object:nil
2098 userInfo:nil];
2099 id mockVC = OCMPartialMock(flutterViewController);
2100 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
2101 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
2102 OCMReject([mockVC sceneDidEnterBackground:[OCMArg any]]);
2103 OCMVerify([mockVC applicationDidEnterBackground:[OCMArg any]]);
2104 XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground);
2105 OCMVerify([mockVC surfaceUpdated:NO]);
2106 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]);
2107 [flutterViewController deregisterNotifications];
2108}
2109
2110- (void)testLifeCycleNotificationSceneDidEnterBackground {
2111 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
2112 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
2113 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
2114 });
2115 FlutterEngine* engine = [[FlutterEngine alloc] init];
2116 [engine runWithEntrypoint:nil];
2117 FlutterViewController* flutterViewController =
2118 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2119 NSNotification* sceneNotification =
2120 [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
2121 object:nil
2122 userInfo:nil];
2123 NSNotification* applicationNotification =
2124 [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
2125 object:nil
2126 userInfo:nil];
2127 id mockVC = OCMPartialMock(flutterViewController);
2128 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
2129 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
2130 OCMVerify([mockVC sceneDidEnterBackground:[OCMArg any]]);
2131 OCMReject([mockVC applicationDidEnterBackground:[OCMArg any]]);
2132 XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground);
2133 OCMVerify([mockVC surfaceUpdated:NO]);
2134 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]);
2135 [flutterViewController deregisterNotifications];
2136 [mockBundle stopMocking];
2137}
2138
2139- (void)testLifeCycleNotificationApplicationWillEnterForeground {
2140 FlutterEngine* engine = [[FlutterEngine alloc] init];
2141 [engine runWithEntrypoint:nil];
2142 FlutterViewController* flutterViewController =
2143 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2144 NSNotification* sceneNotification =
2145 [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
2146 object:nil
2147 userInfo:nil];
2148 NSNotification* applicationNotification =
2149 [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
2150 object:nil
2151 userInfo:nil];
2152 id mockVC = OCMPartialMock(flutterViewController);
2153 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
2154 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
2155 OCMReject([mockVC sceneWillEnterForeground:[OCMArg any]]);
2156 OCMVerify([mockVC applicationWillEnterForeground:[OCMArg any]]);
2157 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2158 [flutterViewController deregisterNotifications];
2159}
2160
2161- (void)testLifeCycleNotificationSceneWillEnterForeground {
2162 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
2163 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
2164 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
2165 });
2166 FlutterEngine* engine = [[FlutterEngine alloc] init];
2167 [engine runWithEntrypoint:nil];
2168 FlutterViewController* flutterViewController =
2169 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2170 NSNotification* sceneNotification =
2171 [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
2172 object:nil
2173 userInfo:nil];
2174 NSNotification* applicationNotification =
2175 [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
2176 object:nil
2177 userInfo:nil];
2178 id mockVC = OCMPartialMock(flutterViewController);
2179 [NSNotificationCenter.defaultCenter postNotification:sceneNotification];
2180 [NSNotificationCenter.defaultCenter postNotification:applicationNotification];
2181 OCMVerify([mockVC sceneWillEnterForeground:[OCMArg any]]);
2182 OCMReject([mockVC applicationWillEnterForeground:[OCMArg any]]);
2183 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2184 [flutterViewController deregisterNotifications];
2185 [mockBundle stopMocking];
2186}
2187
2188- (void)testLifeCycleNotificationCancelledInvalidResumed {
2189 FlutterEngine* engine = [[FlutterEngine alloc] init];
2190 [engine runWithEntrypoint:nil];
2191 FlutterViewController* flutterViewController =
2192 [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2193 NSNotification* applicationDidBecomeActiveNotification =
2194 [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
2195 object:nil
2196 userInfo:nil];
2197 NSNotification* applicationWillResignActiveNotification =
2198 [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
2199 object:nil
2200 userInfo:nil];
2201 id mockVC = OCMPartialMock(flutterViewController);
2202 [NSNotificationCenter.defaultCenter postNotification:applicationDidBecomeActiveNotification];
2203 [NSNotificationCenter.defaultCenter postNotification:applicationWillResignActiveNotification];
2204 OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2205
2206 XCTestExpectation* timeoutApplicationLifeCycle =
2207 [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
2208 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
2209 dispatch_get_main_queue(), ^{
2210 OCMReject([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
2211 [timeoutApplicationLifeCycle fulfill];
2212 [flutterViewController deregisterNotifications];
2213 });
2214 [self waitForExpectationsWithTimeout:5.0 handler:nil];
2215}
2216
2217- (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterViewController {
2218 id bundleMock = OCMPartialMock([NSBundle mainBundle]);
2219 OCMStub([bundleMock objectForInfoDictionaryKey:kCADisableMinimumFrameDurationOnPhoneKey])
2220 .andReturn(@YES);
2221 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2222 double maxFrameRate = 120;
2223 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2224 FlutterEngine* engine = [[FlutterEngine alloc] init];
2225 [engine runWithEntrypoint:nil];
2226 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2227 nibName:nil
2228 bundle:nil];
2230 };
2231 [viewController setUpKeyboardAnimationVsyncClient:callback];
2232 XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2233 CADisplayLink* link = [viewController.keyboardAnimationVSyncClient getDisplayLink];
2234 XCTAssertNotNil(link);
2235 if (@available(iOS 15.0, *)) {
2236 XCTAssertEqual(link.preferredFrameRateRange.maximum, maxFrameRate);
2237 XCTAssertEqual(link.preferredFrameRateRange.preferred, maxFrameRate);
2238 XCTAssertEqual(link.preferredFrameRateRange.minimum, maxFrameRate / 2);
2239 } else {
2240 XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate);
2241 }
2242}
2243
2244- (void)
2245 testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ {
2246 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2247 double maxFrameRate = 120;
2248 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2249 FlutterEngine* engine = [[FlutterEngine alloc] init];
2250 [engine runWithEntrypoint:nil];
2251 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2252 nibName:nil
2253 bundle:nil];
2254 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2255 XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient);
2256}
2257
2258- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists {
2259 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2260 double maxFrameRate = 120;
2261 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2262
2263 FlutterEngine* engine = [[FlutterEngine alloc] init];
2264 [engine runWithEntrypoint:nil];
2265 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2266 nibName:nil
2267 bundle:nil];
2268 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2269 VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient;
2270 XCTAssertNotNil(clientBefore);
2271
2272 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2273 VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient;
2274 XCTAssertNotNil(clientAfter);
2275
2276 XCTAssertTrue(clientBefore == clientAfter);
2277}
2278
2279- (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ {
2280 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2281 double maxFrameRate = 60;
2282 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2283 FlutterEngine* engine = [[FlutterEngine alloc] init];
2284 [engine runWithEntrypoint:nil];
2285 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2286 nibName:nil
2287 bundle:nil];
2288 [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2289 XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
2290}
2291
2292- (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
2293 id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2294 double maxFrameRate = 120;
2295 [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2296 FlutterEngine* engine = [[FlutterEngine alloc] init];
2297 [engine runWithEntrypoint:nil];
2298 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2299 nibName:nil
2300 bundle:nil];
2301 [viewController loadView];
2302 [viewController viewDidLoad];
2303
2304 VSyncClient* client = viewController.touchRateCorrectionVSyncClient;
2305 CADisplayLink* link = [client getDisplayLink];
2306
2307 UITouch* fakeTouchBegan = [[UITouch alloc] init];
2308 fakeTouchBegan.phase = UITouchPhaseBegan;
2309
2310 UITouch* fakeTouchMove = [[UITouch alloc] init];
2311 fakeTouchMove.phase = UITouchPhaseMoved;
2312
2313 UITouch* fakeTouchEnd = [[UITouch alloc] init];
2314 fakeTouchEnd.phase = UITouchPhaseEnded;
2315
2316 UITouch* fakeTouchCancelled = [[UITouch alloc] init];
2317 fakeTouchCancelled.phase = UITouchPhaseCancelled;
2318
2319 [viewController
2320 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]];
2321 XCTAssertFalse(link.isPaused);
2322
2323 [viewController
2324 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]];
2325 XCTAssertTrue(link.isPaused);
2326
2327 [viewController
2328 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]];
2329 XCTAssertFalse(link.isPaused);
2330
2331 [viewController
2332 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]];
2333 XCTAssertTrue(link.isPaused);
2334
2335 [viewController
2336 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2337 initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]];
2338 XCTAssertFalse(link.isPaused);
2339
2340 [viewController
2341 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd,
2342 fakeTouchCancelled, nil]];
2343 XCTAssertTrue(link.isPaused);
2344
2345 [viewController
2346 triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2347 initWithObjects:fakeTouchMove, fakeTouchEnd, nil]];
2348 XCTAssertFalse(link.isPaused);
2349}
2350
2351- (void)testFlutterViewControllerStartKeyboardAnimationWillCreateVsyncClientCorrectly {
2352 FlutterEngine* engine = [[FlutterEngine alloc] init];
2353 [engine runWithEntrypoint:nil];
2354 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2355 nibName:nil
2356 bundle:nil];
2357 viewController.targetViewInsetBottom = 100;
2358 [viewController startKeyBoardAnimation:0.25];
2359 XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2360}
2361
2362- (void)
2363 testSetupKeyboardAnimationVsyncClientWillNotCreateNewVsyncClientWhenKeyboardAnimationCallbackIsNil {
2364 FlutterEngine* engine = [[FlutterEngine alloc] init];
2365 [engine runWithEntrypoint:nil];
2366 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2367 nibName:nil
2368 bundle:nil];
2369 [viewController setUpKeyboardAnimationVsyncClient:nil];
2370 XCTAssertNil(viewController.keyboardAnimationVSyncClient);
2371}
2372
2373- (void)testSupportsShowingSystemContextMenuForIOS16AndAbove {
2374 FlutterEngine* engine = [[FlutterEngine alloc] init];
2375 [engine runWithEntrypoint:nil];
2376 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2377 nibName:nil
2378 bundle:nil];
2379 BOOL supportsShowingSystemContextMenu = [viewController supportsShowingSystemContextMenu];
2380 if (@available(iOS 16.0, *)) {
2381 XCTAssertTrue(supportsShowingSystemContextMenu);
2382 } else {
2383 XCTAssertFalse(supportsShowingSystemContextMenu);
2384 }
2385}
2386
2387- (void)testStateIsActiveAndBackgroundWhenApplicationStateIsActive {
2388 FlutterEngine* engine = [[FlutterEngine alloc] init];
2389 [engine runWithEntrypoint:nil];
2390 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2391 nibName:nil
2392 bundle:nil];
2393 id mockApplication = OCMClassMock([UIApplication class]);
2394 OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateActive);
2395 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
2396 XCTAssertTrue(viewController.stateIsActive);
2397 XCTAssertFalse(viewController.stateIsBackground);
2398}
2399
2400- (void)testStateIsActiveAndBackgroundWhenApplicationStateIsBackground {
2401 FlutterEngine* engine = [[FlutterEngine alloc] init];
2402 [engine runWithEntrypoint:nil];
2403 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2404 nibName:nil
2405 bundle:nil];
2406 id mockApplication = OCMClassMock([UIApplication class]);
2407 OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateBackground);
2408 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
2409 XCTAssertFalse(viewController.stateIsActive);
2410 XCTAssertTrue(viewController.stateIsBackground);
2411}
2412
2413- (void)testStateIsActiveAndBackgroundWhenApplicationStateIsInactive {
2414 FlutterEngine* engine = [[FlutterEngine alloc] init];
2415 [engine runWithEntrypoint:nil];
2416 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2417 nibName:nil
2418 bundle:nil];
2419 id mockApplication = OCMClassMock([UIApplication class]);
2420 OCMStub([mockApplication applicationState]).andReturn(UIApplicationStateInactive);
2421 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
2422 XCTAssertFalse(viewController.stateIsActive);
2423 XCTAssertFalse(viewController.stateIsBackground);
2424}
2425
2426- (void)testStateIsActiveAndBackgroundWhenSceneStateIsActive {
2427 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
2428 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
2429 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
2430 });
2431 FlutterEngine* engine = [[FlutterEngine alloc] init];
2432 [engine runWithEntrypoint:nil];
2433 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2434 nibName:nil
2435 bundle:nil];
2436 id mockVC = OCMPartialMock(viewController);
2437 OCMStub([mockVC activationState]).andReturn(UISceneActivationStateForegroundActive);
2438 XCTAssertTrue(viewController.stateIsActive);
2439 XCTAssertFalse(viewController.stateIsBackground);
2440
2441 [mockBundle stopMocking];
2442 [mockVC stopMocking];
2443}
2444
2445- (void)testStateIsActiveAndBackgroundWhenSceneStateIsBackground {
2446 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
2447 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
2448 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
2449 });
2450 FlutterEngine* engine = [[FlutterEngine alloc] init];
2451 [engine runWithEntrypoint:nil];
2452 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2453 nibName:nil
2454 bundle:nil];
2455 id mockVC = OCMPartialMock(viewController);
2456 OCMStub([mockVC activationState]).andReturn(UISceneActivationStateBackground);
2457 XCTAssertFalse(viewController.stateIsActive);
2458 XCTAssertTrue(viewController.stateIsBackground);
2459
2460 [mockBundle stopMocking];
2461 [mockVC stopMocking];
2462}
2463
2464- (void)testStateIsActiveAndBackgroundWhenSceneStateIsInactive {
2465 id mockBundle = OCMPartialMock([NSBundle mainBundle]);
2466 OCMStub([mockBundle objectForInfoDictionaryKey:@"NSExtension"]).andReturn(@{
2467 @"NSExtensionPointIdentifier" : @"com.apple.share-services"
2468 });
2469 FlutterEngine* engine = [[FlutterEngine alloc] init];
2470 [engine runWithEntrypoint:nil];
2471 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2472 nibName:nil
2473 bundle:nil];
2474 id mockVC = OCMPartialMock(viewController);
2475 OCMStub([mockVC activationState]).andReturn(UISceneActivationStateForegroundInactive);
2476 XCTAssertFalse(viewController.stateIsActive);
2477 XCTAssertFalse(viewController.stateIsBackground);
2478
2479 [mockBundle stopMocking];
2480 [mockVC stopMocking];
2481}
2482
2483- (void)testPerformImplicitEngineCallbacks {
2484 id mockRegistrant = OCMProtocolMock(@protocol(FlutterPluginRegistrant));
2485 id appDelegate = [[UIApplication sharedApplication] delegate];
2486 [appDelegate setMockLaunchEngine:self.mockEngine];
2487 UIStoryboard* storyboard = [UIStoryboard storyboardWithName:@"Flutter" bundle:nil];
2488 XCTAssertTrue([appDelegate respondsToSelector:@selector(setPluginRegistrant:)]);
2489 [appDelegate setPluginRegistrant:mockRegistrant];
2491 (FlutterViewController*)[storyboard instantiateInitialViewController];
2492 [appDelegate setPluginRegistrant:nil];
2493 OCMVerify([mockRegistrant registerWithRegistry:viewController]);
2494 OCMVerify([self.mockEngine performImplicitEngineCallback]);
2495 [appDelegate setMockLaunchEngine:nil];
2496}
2497
2498- (void)testPerformImplicitEngineCallbacksUsesAppLaunchEventFallbacks {
2499 id mockEngine = OCMClassMock([FlutterEngine class]);
2500 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
2501 nibName:nil
2502 bundle:nil];
2503 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
2504 OCMStub([mockEngine performImplicitEngineCallback]).andReturn(YES);
2505 OCMStub([viewControllerMock awokenFromNib]).andReturn(YES);
2506
2507 id mockApplication = OCMClassMock([UIApplication class]);
2508 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
2509 FlutterAppDelegate* mockApplicationDelegate = OCMClassMock([FlutterAppDelegate class]);
2510 OCMStub([mockApplication delegate]).andReturn(mockApplicationDelegate);
2511 OCMStub([mockApplicationDelegate takeLaunchEngine]).andReturn(mockEngine);
2512
2513 id mockScene = OCMClassMock([UIScene class]);
2514 id mockSceneDelegate = OCMProtocolMock(@protocol(UISceneDelegate));
2515 OCMStub([mockScene delegate]).andReturn(mockSceneDelegate);
2516 OCMStub([mockApplication connectedScenes]).andReturn([NSSet setWithObject:mockScene]);
2517
2518 FlutterPluginAppLifeCycleDelegate* mockLifecycleDelegate =
2519 OCMClassMock([FlutterPluginAppLifeCycleDelegate class]);
2520 OCMStub([mockApplicationDelegate lifeCycleDelegate]).andReturn(mockLifecycleDelegate);
2521
2522 [viewControllerMock sharedSetupWithProject:nil initialRoute:nil];
2523 OCMVerify([mockLifecycleDelegate sceneFallbackWillFinishLaunchingApplication:mockApplication]);
2524 OCMVerify([mockLifecycleDelegate sceneFallbackDidFinishLaunchingApplication:mockApplication]);
2525}
2526
2527- (void)testPerformImplicitEngineCallbacksNoAppLaunchEventFallbacksWhenNoStoryboard {
2528 id mockEngine = OCMClassMock([FlutterEngine class]);
2529 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
2530 nibName:nil
2531 bundle:nil];
2532 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
2533 OCMStub([mockEngine performImplicitEngineCallback]).andReturn(YES);
2534 OCMStub([viewControllerMock awokenFromNib]).andReturn(NO);
2535
2536 id mockApplication = OCMClassMock([UIApplication class]);
2537 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
2538 FlutterAppDelegate* mockApplicationDelegate = OCMClassMock([FlutterAppDelegate class]);
2539 OCMStub([mockApplication delegate]).andReturn(mockApplicationDelegate);
2540 OCMStub([mockApplicationDelegate takeLaunchEngine]).andReturn(mockEngine);
2541
2542 id mockScene = OCMClassMock([UIScene class]);
2543 id mockSceneDelegate = OCMProtocolMock(@protocol(UISceneDelegate));
2544 OCMStub([mockScene delegate]).andReturn(mockSceneDelegate);
2545 OCMStub([mockApplication connectedScenes]).andReturn([NSSet setWithObject:mockScene]);
2546
2547 FlutterPluginAppLifeCycleDelegate* mockLifecycleDelegate =
2548 OCMClassMock([FlutterPluginAppLifeCycleDelegate class]);
2549 OCMStub([mockApplicationDelegate lifeCycleDelegate]).andReturn(mockLifecycleDelegate);
2550
2551 [viewControllerMock sharedSetupWithProject:nil initialRoute:nil];
2552 OCMReject([mockLifecycleDelegate sceneFallbackWillFinishLaunchingApplication:mockApplication]);
2553 OCMReject([mockLifecycleDelegate sceneFallbackDidFinishLaunchingApplication:mockApplication]);
2554}
2555
2556- (void)testPerformImplicitEngineCallbacksNoAppLaunchEventFallbacksWhenNoScenes {
2557 id mockEngine = OCMClassMock([FlutterEngine class]);
2558 FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
2559 nibName:nil
2560 bundle:nil];
2561 FlutterViewController* viewControllerMock = OCMPartialMock(viewController);
2562 OCMStub([mockEngine performImplicitEngineCallback]).andReturn(YES);
2563 OCMStub([viewControllerMock awokenFromNib]).andReturn(YES);
2564
2565 id mockApplication = OCMClassMock([UIApplication class]);
2566 OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
2567 FlutterAppDelegate* mockApplicationDelegate = OCMClassMock([FlutterAppDelegate class]);
2568 OCMStub([mockApplication delegate]).andReturn(mockApplicationDelegate);
2569 OCMStub([mockApplicationDelegate takeLaunchEngine]).andReturn(mockEngine);
2570
2571 FlutterPluginAppLifeCycleDelegate* mockLifecycleDelegate =
2572 OCMClassMock([FlutterPluginAppLifeCycleDelegate class]);
2573 OCMStub([mockApplicationDelegate lifeCycleDelegate]).andReturn(mockLifecycleDelegate);
2574
2575 [viewControllerMock sharedSetupWithProject:nil initialRoute:nil];
2576 OCMReject([mockLifecycleDelegate sceneFallbackWillFinishLaunchingApplication:mockApplication]);
2577 OCMReject([mockLifecycleDelegate sceneFallbackDidFinishLaunchingApplication:mockApplication]);
2578}
2579
2580- (void)testGrabLaunchEngine {
2581 id appDelegate = [[UIApplication sharedApplication] delegate];
2582 XCTAssertTrue([appDelegate respondsToSelector:@selector(setMockLaunchEngine:)]);
2583 [appDelegate setMockLaunchEngine:self.mockEngine];
2584 UIStoryboard* storyboard = [UIStoryboard storyboardWithName:@"Flutter" bundle:nil];
2585 XCTAssertTrue(storyboard);
2587 (FlutterViewController*)[storyboard instantiateInitialViewController];
2588 XCTAssertTrue(viewController);
2589 XCTAssertTrue([viewController isKindOfClass:[FlutterViewController class]]);
2590 XCTAssertEqual(viewController.engine, self.mockEngine);
2591 [appDelegate setMockLaunchEngine:nil];
2592}
2593
2594- (void)testDoesntGrabLaunchEngine {
2595 id appDelegate = [[UIApplication sharedApplication] delegate];
2596 XCTAssertTrue([appDelegate respondsToSelector:@selector(setMockLaunchEngine:)]);
2597 [appDelegate setMockLaunchEngine:self.mockEngine];
2598 FlutterViewController* flutterViewController = [[FlutterViewController alloc] init];
2599 XCTAssertNotNil(flutterViewController.engine);
2600 XCTAssertNotEqual(flutterViewController.engine, self.mockEngine);
2601 [appDelegate setMockLaunchEngine:nil];
2602}
2603
2604@end
NS_ASSUME_NONNULL_BEGIN typedef void(^ FlutterReply)(id _Nullable reply)
static CFStringRef kMessageLoopCFRunLoopMode
void(* FlutterKeyEventCallback)(bool, void *)
Definition embedder.h:1427
GLFWwindow * window
Definition main.cc:60
FlutterEngine engine
Definition main.cc:84
FlView * view
G_BEGIN_DECLS GBytes * message
FlutterDesktopBinaryReply callback
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
FlutterViewController * viewController
FlutterTextInputPlugin * textInputPlugin
FlutterBasicMessageChannel * lifecycleChannel
FlutterViewController * viewController
FlutterBasicMessageChannel * keyEventChannel
NSObject< FlutterBinaryMessenger > * binaryMessenger
void(^ FlutterSendKeyEvent)(const FlutterKeyEvent &, _Nullable FlutterKeyEventCallback, void *_Nullable)
UITextSmartQuotesType smartQuotesType API_AVAILABLE(ios(11.0))
FlutterViewController * viewController
FlutterTextInputPlugin * textInputPlugin
NSNotificationName const FlutterViewControllerWillDealloc
void(^ FlutterKeyboardAnimationCallback)(fml::TimePoint)
Definition ref_ptr.h:261
const uintptr_t id
#define NSEC_PER_SEC
Definition timerfd.cc:35
NSString *const kCADisableMinimumFrameDurationOnPhoneKey
Info.plist key enabling the full range of ProMotion refresh rates for CADisplayLink callbacks and CAA...
int BOOL