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