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