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