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