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