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