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