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"
21 #import "flutter/shell/platform/embedder/embedder.h"
22 #import "flutter/third_party/spring_animation/spring_animation.h"
23 
25 
26 using namespace flutter::testing;
27 
28 @interface FlutterEngine ()
30 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
31  callback:(nullable FlutterKeyEventCallback)callback
32  userData:(nullable void*)userData;
33 - (fml::RefPtr<fml::TaskRunner>)uiTaskRunner;
34 @end
35 
36 /// Sometimes we have to use a custom mock to avoid retain cycles in OCMock.
37 /// Used for testing low memory notification.
39 @property(nonatomic, strong) FlutterBasicMessageChannel* lifecycleChannel;
40 @property(nonatomic, strong) FlutterBasicMessageChannel* keyEventChannel;
41 @property(nonatomic, weak) FlutterViewController* viewController;
42 @property(nonatomic, strong) FlutterTextInputPlugin* textInputPlugin;
43 @property(nonatomic, assign) BOOL didCallNotifyLowMemory;
45 - (void)sendKeyEvent:(const FlutterKeyEvent&)event
46  callback:(nullable FlutterKeyEventCallback)callback
47  userData:(nullable void*)userData;
48 @end
49 
50 @implementation FlutterEnginePartialMock
51 @synthesize viewController;
52 @synthesize lifecycleChannel;
53 @synthesize keyEventChannel;
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  @autoreleasepool {
1140  weakViewController = viewController;
1141  [viewController viewDidLoad];
1142  }
1143  XCTAssertNil(weakViewController);
1144 }
1145 
1146 #pragma mark - Platform Brightness
1147 
1148 - (void)testItReportsLightPlatformBrightnessByDefault {
1149  // Setup test.
1150  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1151  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1152 
1153  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1154  nibName:nil
1155  bundle:nil];
1156 
1157  // Exercise behavior under test.
1158  [vc traitCollectionDidChange:nil];
1159 
1160  // Verify behavior.
1161  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1162  return [message[@"platformBrightness"] isEqualToString:@"light"];
1163  }]]);
1164 
1165  // Clean up mocks
1166  [settingsChannel stopMocking];
1167 }
1168 
1169 - (void)testItReportsPlatformBrightnessWhenViewWillAppear {
1170  // Setup test.
1171  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1172  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1173  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1174  OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
1175  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1176  nibName:nil
1177  bundle:nil];
1178 
1179  // Exercise behavior under test.
1180  [vc viewWillAppear:false];
1181 
1182  // Verify behavior.
1183  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1184  return [message[@"platformBrightness"] isEqualToString:@"light"];
1185  }]]);
1186 
1187  // Clean up mocks
1188  [settingsChannel stopMocking];
1189 }
1190 
1191 - (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt {
1192  if (@available(iOS 13, *)) {
1193  // noop
1194  } else {
1195  return;
1196  }
1197 
1198  // Setup test.
1199  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1200  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1201  id mockTraitCollection =
1202  [self fakeTraitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark];
1203 
1204  // We partially mock the real FlutterViewController to act as the OS and report
1205  // the UITraitCollection of our choice. Mocking the object under test is not
1206  // desirable, but given that the OS does not offer a DI approach to providing
1207  // our own UITraitCollection, this seems to be the least bad option.
1208  id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine
1209  nibName:nil
1210  bundle:nil]);
1211  OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
1212 
1213  // Exercise behavior under test.
1214  [partialMockVC traitCollectionDidChange:nil];
1215 
1216  // Verify behavior.
1217  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1218  return [message[@"platformBrightness"] isEqualToString:@"dark"];
1219  }]]);
1220 
1221  // Clean up mocks
1222  [partialMockVC stopMocking];
1223  [settingsChannel stopMocking];
1224  [mockTraitCollection stopMocking];
1225 }
1226 
1227 // Creates a mocked UITraitCollection with nil values for everything except userInterfaceStyle,
1228 // which is set to the given "style".
1229 - (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style {
1230  id mockTraitCollection = OCMClassMock([UITraitCollection class]);
1231  OCMStub([mockTraitCollection userInterfaceStyle]).andReturn(style);
1232  return mockTraitCollection;
1233 }
1234 
1235 #pragma mark - Platform Contrast
1236 
1237 - (void)testItReportsNormalPlatformContrastByDefault {
1238  if (@available(iOS 13, *)) {
1239  // noop
1240  } else {
1241  return;
1242  }
1243 
1244  // Setup test.
1245  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1246  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1247 
1248  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1249  nibName:nil
1250  bundle:nil];
1251 
1252  // Exercise behavior under test.
1253  [vc traitCollectionDidChange:nil];
1254 
1255  // Verify behavior.
1256  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1257  return [message[@"platformContrast"] isEqualToString:@"normal"];
1258  }]]);
1259 
1260  // Clean up mocks
1261  [settingsChannel stopMocking];
1262 }
1263 
1264 - (void)testItReportsPlatformContrastWhenViewWillAppear {
1265  if (@available(iOS 13, *)) {
1266  // noop
1267  } else {
1268  return;
1269  }
1270  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1271  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1272 
1273  // Setup test.
1274  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1275  OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel);
1276  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1277  nibName:nil
1278  bundle:nil];
1279 
1280  // Exercise behavior under test.
1281  [vc viewWillAppear:false];
1282 
1283  // Verify behavior.
1284  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1285  return [message[@"platformContrast"] isEqualToString:@"normal"];
1286  }]]);
1287 
1288  // Clean up mocks
1289  [settingsChannel stopMocking];
1290 }
1291 
1292 - (void)testItReportsHighContrastWhenTraitCollectionRequestsIt {
1293  if (@available(iOS 13, *)) {
1294  // noop
1295  } else {
1296  return;
1297  }
1298 
1299  // Setup test.
1300  id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1301  OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel);
1302 
1303  id mockTraitCollection = [self fakeTraitCollectionWithContrast:UIAccessibilityContrastHigh];
1304 
1305  // We partially mock the real FlutterViewController to act as the OS and report
1306  // the UITraitCollection of our choice. Mocking the object under test is not
1307  // desirable, but given that the OS does not offer a DI approach to providing
1308  // our own UITraitCollection, this seems to be the least bad option.
1309  id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine
1310  nibName:nil
1311  bundle:nil]);
1312  OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection);
1313 
1314  // Exercise behavior under test.
1315  [partialMockVC traitCollectionDidChange:mockTraitCollection];
1316 
1317  // Verify behavior.
1318  OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) {
1319  return [message[@"platformContrast"] isEqualToString:@"high"];
1320  }]]);
1321 
1322  // Clean up mocks
1323  [partialMockVC stopMocking];
1324  [settingsChannel stopMocking];
1325  [mockTraitCollection stopMocking];
1326 }
1327 
1328 - (void)testItReportsAccessibilityOnOffSwitchLabelsFlagNotSet {
1329  if (@available(iOS 13, *)) {
1330  // noop
1331  } else {
1332  return;
1333  }
1334 
1335  // Setup test.
1337  [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
1338  id partialMockViewController = OCMPartialMock(viewController);
1339  OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(NO);
1340 
1341  // Exercise behavior under test.
1342  int32_t flags = [partialMockViewController accessibilityFlags];
1343 
1344  // Verify behavior.
1345  XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) == 0);
1346 }
1347 
1348 - (void)testItReportsAccessibilityOnOffSwitchLabelsFlagSet {
1349  if (@available(iOS 13, *)) {
1350  // noop
1351  } else {
1352  return;
1353  }
1354 
1355  // Setup test.
1357  [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil];
1358  id partialMockViewController = OCMPartialMock(viewController);
1359  OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(YES);
1360 
1361  // Exercise behavior under test.
1362  int32_t flags = [partialMockViewController accessibilityFlags];
1363 
1364  // Verify behavior.
1365  XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) != 0);
1366 }
1367 
1368 - (void)testPerformOrientationUpdateForcesOrientationChange {
1369  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1370  currentOrientation:UIInterfaceOrientationLandscapeLeft
1371  didChangeOrientation:YES
1372  resultingOrientation:UIInterfaceOrientationPortrait];
1373 
1374  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1375  currentOrientation:UIInterfaceOrientationLandscapeRight
1376  didChangeOrientation:YES
1377  resultingOrientation:UIInterfaceOrientationPortrait];
1378 
1379  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1380  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1381  didChangeOrientation:YES
1382  resultingOrientation:UIInterfaceOrientationPortrait];
1383 
1384  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1385  currentOrientation:UIInterfaceOrientationLandscapeLeft
1386  didChangeOrientation:YES
1387  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1388 
1389  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1390  currentOrientation:UIInterfaceOrientationLandscapeRight
1391  didChangeOrientation:YES
1392  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1393 
1394  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1395  currentOrientation:UIInterfaceOrientationPortrait
1396  didChangeOrientation:YES
1397  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1398 
1399  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1400  currentOrientation:UIInterfaceOrientationPortrait
1401  didChangeOrientation:YES
1402  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1403 
1404  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1405  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1406  didChangeOrientation:YES
1407  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1408 
1409  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1410  currentOrientation:UIInterfaceOrientationPortrait
1411  didChangeOrientation:YES
1412  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1413 
1414  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1415  currentOrientation:UIInterfaceOrientationLandscapeRight
1416  didChangeOrientation:YES
1417  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1418 
1419  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1420  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1421  didChangeOrientation:YES
1422  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1423 
1424  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1425  currentOrientation:UIInterfaceOrientationPortrait
1426  didChangeOrientation:YES
1427  resultingOrientation:UIInterfaceOrientationLandscapeRight];
1428 
1429  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1430  currentOrientation:UIInterfaceOrientationLandscapeLeft
1431  didChangeOrientation:YES
1432  resultingOrientation:UIInterfaceOrientationLandscapeRight];
1433 
1434  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1435  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1436  didChangeOrientation:YES
1437  resultingOrientation:UIInterfaceOrientationLandscapeRight];
1438 
1439  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1440  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1441  didChangeOrientation:YES
1442  resultingOrientation:UIInterfaceOrientationPortrait];
1443 }
1444 
1445 - (void)testPerformOrientationUpdateDoesNotForceOrientationChange {
1446  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1447  currentOrientation:UIInterfaceOrientationPortrait
1448  didChangeOrientation:NO
1449  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1450 
1451  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1452  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1453  didChangeOrientation:NO
1454  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1455 
1456  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1457  currentOrientation:UIInterfaceOrientationLandscapeLeft
1458  didChangeOrientation:NO
1459  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1460 
1461  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1462  currentOrientation:UIInterfaceOrientationLandscapeRight
1463  didChangeOrientation:NO
1464  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1465 
1466  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1467  currentOrientation:UIInterfaceOrientationPortrait
1468  didChangeOrientation:NO
1469  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1470 
1471  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1472  currentOrientation:UIInterfaceOrientationLandscapeLeft
1473  didChangeOrientation:NO
1474  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1475 
1476  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1477  currentOrientation:UIInterfaceOrientationLandscapeRight
1478  didChangeOrientation:NO
1479  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1480 
1481  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1482  currentOrientation:UIInterfaceOrientationPortrait
1483  didChangeOrientation:NO
1484  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1485 
1486  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1487  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1488  didChangeOrientation:NO
1489  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1490 
1491  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1492  currentOrientation:UIInterfaceOrientationLandscapeLeft
1493  didChangeOrientation:NO
1494  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1495 
1496  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1497  currentOrientation:UIInterfaceOrientationLandscapeRight
1498  didChangeOrientation:NO
1499  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1500 
1501  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1502  currentOrientation:UIInterfaceOrientationLandscapeLeft
1503  didChangeOrientation:NO
1504  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1505 
1506  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1507  currentOrientation:UIInterfaceOrientationLandscapeRight
1508  didChangeOrientation:NO
1509  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1510 }
1511 
1512 // Perform an orientation update test that fails when the expected outcome
1513 // for an orientation update is not met
1514 - (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask
1515  currentOrientation:(UIInterfaceOrientation)currentOrientation
1516  didChangeOrientation:(BOOL)didChange
1517  resultingOrientation:(UIInterfaceOrientation)resultingOrientation {
1518  id mockApplication = OCMClassMock([UIApplication class]);
1519  id mockWindowScene;
1520  id deviceMock;
1521  id mockVC;
1522  __block __weak id weakPreferences;
1523  @autoreleasepool {
1524  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1525  nibName:nil
1526  bundle:nil];
1527 
1528  if (@available(iOS 16.0, *)) {
1529  mockWindowScene = OCMClassMock([UIWindowScene class]);
1530  mockVC = OCMPartialMock(realVC);
1531  OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1532  if (realVC.supportedInterfaceOrientations == mask) {
1533  OCMReject([mockWindowScene requestGeometryUpdateWithPreferences:[OCMArg any]
1534  errorHandler:[OCMArg any]]);
1535  } else {
1536  // iOS 16 will decide whether to rotate based on the new preference, so always set it
1537  // when it changes.
1538  OCMExpect([mockWindowScene
1539  requestGeometryUpdateWithPreferences:[OCMArg checkWithBlock:^BOOL(
1540  UIWindowSceneGeometryPreferencesIOS*
1541  preferences) {
1542  weakPreferences = preferences;
1543  return preferences.interfaceOrientations == mask;
1544  }]
1545  errorHandler:[OCMArg any]]);
1546  }
1547  OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
1548  OCMStub([mockApplication connectedScenes]).andReturn([NSSet setWithObject:mockWindowScene]);
1549  } else {
1550  deviceMock = OCMPartialMock([UIDevice currentDevice]);
1551  if (!didChange) {
1552  OCMReject([deviceMock setValue:[OCMArg any] forKey:@"orientation"]);
1553  } else {
1554  OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]);
1555  }
1556  if (@available(iOS 13.0, *)) {
1557  mockWindowScene = OCMClassMock([UIWindowScene class]);
1558  mockVC = OCMPartialMock(realVC);
1559  OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1560  OCMStub(((UIWindowScene*)mockWindowScene).interfaceOrientation)
1561  .andReturn(currentOrientation);
1562  } else {
1563  OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
1564  OCMStub([mockApplication statusBarOrientation]).andReturn(currentOrientation);
1565  }
1566  }
1567 
1568  [realVC performOrientationUpdate:mask];
1569  if (@available(iOS 16.0, *)) {
1570  OCMVerifyAll(mockWindowScene);
1571  } else {
1572  OCMVerifyAll(deviceMock);
1573  }
1574  }
1575  [mockWindowScene stopMocking];
1576  [deviceMock stopMocking];
1577  [mockApplication stopMocking];
1578  XCTAssertNil(weakPreferences);
1579 }
1580 
1581 // Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast,
1582 // which is set to the given "contrast".
1583 - (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast {
1584  id mockTraitCollection = OCMClassMock([UITraitCollection class]);
1585  OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast);
1586  return mockTraitCollection;
1587 }
1588 
1589 - (void)testWillDeallocNotification {
1590  XCTestExpectation* expectation =
1591  [[XCTestExpectation alloc] initWithDescription:@"notification called"];
1592  id engine = [[MockEngine alloc] init];
1593  @autoreleasepool {
1594  // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
1595  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1596  nibName:nil
1597  bundle:nil];
1598  [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc
1599  object:nil
1600  queue:[NSOperationQueue mainQueue]
1601  usingBlock:^(NSNotification* _Nonnull note) {
1602  [expectation fulfill];
1603  }];
1604  XCTAssertNotNil(realVC);
1605  realVC = nil;
1606  }
1607  [self waitForExpectations:@[ expectation ] timeout:1.0];
1608 }
1609 
1610 - (void)testReleasesKeyboardManagerOnDealloc {
1611  __weak FlutterKeyboardManager* weakKeyboardManager = nil;
1612  @autoreleasepool {
1614 
1615  [viewController addInternalPlugins];
1616  weakKeyboardManager = viewController.keyboardManager;
1617  XCTAssertNotNil(weakKeyboardManager);
1618  [viewController deregisterNotifications];
1619  viewController = nil;
1620  }
1621  // View controller has released the keyboard manager.
1622  XCTAssertNil(weakKeyboardManager);
1623 }
1624 
1625 - (void)testDoesntLoadViewInInit {
1626  FlutterDartProject* project = [[FlutterDartProject alloc] init];
1627  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1628  [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1629  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1630  nibName:nil
1631  bundle:nil];
1632  XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown");
1633  engine.viewController = nil;
1634 }
1635 
1636 - (void)testHideOverlay {
1637  FlutterDartProject* project = [[FlutterDartProject alloc] init];
1638  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1639  [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1640  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1641  nibName:nil
1642  bundle:nil];
1643  XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @"");
1644  [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator
1645  object:nil];
1646  XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @"");
1647  engine.viewController = nil;
1648 }
1649 
1650 - (void)testNotifyLowMemory {
1652  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1653  nibName:nil
1654  bundle:nil];
1655  id viewControllerMock = OCMPartialMock(viewController);
1656  OCMStub([viewControllerMock surfaceUpdated:NO]);
1657  [viewController beginAppearanceTransition:NO animated:NO];
1658  [viewController endAppearanceTransition];
1659  XCTAssertTrue(mockEngine.didCallNotifyLowMemory);
1660 }
1661 
1662 - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback {
1663  NSMutableDictionary* replyMessage = [@{
1664  @"handled" : @YES,
1665  } mutableCopy];
1666  // Response is async, so we have to post it to the run loop instead of calling
1667  // it directly.
1668  self.messageSent = message;
1669  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
1670  ^() {
1671  callback(replyMessage);
1672  });
1673 }
1674 
1675 - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {
1676  if (@available(iOS 13.4, *)) {
1677  // noop
1678  } else {
1679  return;
1680  }
1682  mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1683  OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1684  .andCall(self, @selector(sendMessage:reply:));
1685  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1686  mockEngine.textInputPlugin = self.mockTextInputPlugin;
1687 
1688  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1689  nibName:nil
1690  bundle:nil];
1691 
1692  // Allocate the keyboard manager in the view controller by adding the internal
1693  // plugins.
1694  [vc addInternalPlugins];
1695 
1696  [vc handlePressEvent:keyUpEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0)
1697  nextAction:^(){
1698  }];
1699 
1700  XCTAssert(self.messageSent != nil);
1701  XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1702  XCTAssert([self.messageSent[@"type"] isEqualToString:@"keyup"]);
1703  XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1704  XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1705  XCTAssert([self.messageSent[@"characters"] isEqualToString:@""]);
1706  XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@""]);
1707  [vc deregisterNotifications];
1708 }
1709 
1710 - (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) {
1711  if (@available(iOS 13.4, *)) {
1712  // noop
1713  } else {
1714  return;
1715  }
1716 
1718  mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1719  OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1720  .andCall(self, @selector(sendMessage:reply:));
1721  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1722  mockEngine.textInputPlugin = self.mockTextInputPlugin;
1723 
1724  __strong FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1725  nibName:nil
1726  bundle:nil];
1727  // Allocate the keyboard manager in the view controller by adding the internal
1728  // plugins.
1729  [vc addInternalPlugins];
1730 
1731  [vc handlePressEvent:keyDownEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0f, "A",
1732  "a")
1733  nextAction:^(){
1734  }];
1735 
1736  XCTAssert(self.messageSent != nil);
1737  XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1738  XCTAssert([self.messageSent[@"type"] isEqualToString:@"keydown"]);
1739  XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1740  XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1741  XCTAssert([self.messageSent[@"characters"] isEqualToString:@"A"]);
1742  XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@"a"]);
1743  [vc deregisterNotifications];
1744  vc = nil;
1745 }
1746 
1747 - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) {
1748  if (@available(iOS 13.4, *)) {
1749  // noop
1750  } else {
1751  return;
1752  }
1753  id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1754  OCMStub([keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1755  .andCall(self, @selector(sendMessage:reply:));
1756  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1757  OCMStub([self.mockEngine keyEventChannel]).andReturn(keyEventChannel);
1758 
1759  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1760  nibName:nil
1761  bundle:nil];
1762 
1763  // Allocate the keyboard manager in the view controller by adding the internal
1764  // plugins.
1765  [vc addInternalPlugins];
1766 
1767  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseStationary, UIKeyboardHIDUsageKeyboardA,
1768  UIKeyModifierShift, 123.0)
1769  nextAction:^(){
1770  }];
1771  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseCancelled, UIKeyboardHIDUsageKeyboardA,
1772  UIKeyModifierShift, 123.0)
1773  nextAction:^(){
1774  }];
1775  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseChanged, UIKeyboardHIDUsageKeyboardA,
1776  UIKeyModifierShift, 123.0)
1777  nextAction:^(){
1778  }];
1779 
1780  XCTAssert(self.messageSent == nil);
1781  OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]);
1782  [vc deregisterNotifications];
1783 }
1784 
1785 - (void)testPanGestureRecognizer API_AVAILABLE(ios(13.4)) {
1786  if (@available(iOS 13.4, *)) {
1787  // noop
1788  } else {
1789  return;
1790  }
1791 
1792  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1793  nibName:nil
1794  bundle:nil];
1795  XCTAssertNotNil(vc);
1796  UIView* view = vc.view;
1797  XCTAssertNotNil(view);
1798  NSArray* gestureRecognizers = view.gestureRecognizers;
1799  XCTAssertNotNil(gestureRecognizers);
1800 
1801  BOOL found = NO;
1802  for (id gesture in gestureRecognizers) {
1803  if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
1804  found = YES;
1805  break;
1806  }
1807  }
1808  XCTAssertTrue(found);
1809 }
1810 
1811 - (void)testMouseSupport API_AVAILABLE(ios(13.4)) {
1812  if (@available(iOS 13.4, *)) {
1813  // noop
1814  } else {
1815  return;
1816  }
1817 
1818  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1819  nibName:nil
1820  bundle:nil];
1821  XCTAssertNotNil(vc);
1822 
1823  id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]);
1824  XCTAssertNotNil(mockPanGestureRecognizer);
1825 
1826  [vc discreteScrollEvent:mockPanGestureRecognizer];
1827 
1828  [[[self.mockEngine verify] ignoringNonObjectArgs]
1829  dispatchPointerDataPacket:std::make_unique<flutter::PointerDataPacket>(0)];
1830 }
1831 
1832 - (void)testFakeEventTimeStamp {
1833  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1834  nibName:nil
1835  bundle:nil];
1836  XCTAssertNotNil(vc);
1837 
1838  flutter::PointerData pointer_data = [vc generatePointerDataForFake];
1839  int64_t current_micros = [[NSProcessInfo processInfo] systemUptime] * 1000 * 1000;
1840  int64_t interval_micros = current_micros - pointer_data.time_stamp;
1841  const int64_t tolerance_millis = 2;
1842  XCTAssertTrue(interval_micros / 1000 < tolerance_millis,
1843  @"PointerData.time_stamp should be equal to NSProcessInfo.systemUptime");
1844 }
1845 
1846 - (void)testSplashScreenViewCanSetNil {
1847  FlutterViewController* flutterViewController =
1848  [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
1849  [flutterViewController setSplashScreenView:nil];
1850 }
1851 
1852 - (void)testLifeCycleNotificationBecameActive {
1853  FlutterEngine* engine = [[FlutterEngine alloc] init];
1854  [engine runWithEntrypoint:nil];
1855  FlutterViewController* flutterViewController =
1856  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1857  UIWindow* window = [[UIWindow alloc] init];
1858  [window addSubview:flutterViewController.view];
1859  flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100);
1860  [flutterViewController viewDidLayoutSubviews];
1861  NSNotification* sceneNotification =
1862  [NSNotification notificationWithName:UISceneDidActivateNotification object:nil userInfo:nil];
1863  NSNotification* applicationNotification =
1864  [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
1865  object:nil
1866  userInfo:nil];
1867  id mockVC = OCMPartialMock(flutterViewController);
1868  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1869  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1870 #if APPLICATION_EXTENSION_API_ONLY
1871  OCMVerify([mockVC sceneBecameActive:[OCMArg any]]);
1872  OCMReject([mockVC applicationBecameActive:[OCMArg any]]);
1873 #else
1874  OCMReject([mockVC sceneBecameActive:[OCMArg any]]);
1875  OCMVerify([mockVC applicationBecameActive:[OCMArg any]]);
1876 #endif
1877  XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1878  OCMVerify([mockVC surfaceUpdated:YES]);
1879  XCTestExpectation* timeoutApplicationLifeCycle =
1880  [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
1881  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
1882  dispatch_get_main_queue(), ^{
1883  [timeoutApplicationLifeCycle fulfill];
1884  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
1885  [flutterViewController deregisterNotifications];
1886  });
1887  [self waitForExpectationsWithTimeout:5.0 handler:nil];
1888 }
1889 
1890 - (void)testLifeCycleNotificationWillResignActive {
1891  FlutterEngine* engine = [[FlutterEngine alloc] init];
1892  [engine runWithEntrypoint:nil];
1893  FlutterViewController* flutterViewController =
1894  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1895  NSNotification* sceneNotification =
1896  [NSNotification notificationWithName:UISceneWillDeactivateNotification
1897  object:nil
1898  userInfo:nil];
1899  NSNotification* applicationNotification =
1900  [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
1901  object:nil
1902  userInfo:nil];
1903  id mockVC = OCMPartialMock(flutterViewController);
1904  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1905  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1906 #if APPLICATION_EXTENSION_API_ONLY
1907  OCMVerify([mockVC sceneWillResignActive:[OCMArg any]]);
1908  OCMReject([mockVC applicationWillResignActive:[OCMArg any]]);
1909 #else
1910  OCMReject([mockVC sceneWillResignActive:[OCMArg any]]);
1911  OCMVerify([mockVC applicationWillResignActive:[OCMArg any]]);
1912 #endif
1913  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
1914  [flutterViewController deregisterNotifications];
1915 }
1916 
1917 - (void)testLifeCycleNotificationWillTerminate {
1918  FlutterEngine* engine = [[FlutterEngine alloc] init];
1919  [engine runWithEntrypoint:nil];
1920  FlutterViewController* flutterViewController =
1921  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1922  NSNotification* sceneNotification =
1923  [NSNotification notificationWithName:UISceneDidDisconnectNotification
1924  object:nil
1925  userInfo:nil];
1926  NSNotification* applicationNotification =
1927  [NSNotification notificationWithName:UIApplicationWillTerminateNotification
1928  object:nil
1929  userInfo:nil];
1930  id mockVC = OCMPartialMock(flutterViewController);
1931  id mockEngine = OCMPartialMock(engine);
1932  OCMStub([mockVC engine]).andReturn(mockEngine);
1933  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1934  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1935 #if APPLICATION_EXTENSION_API_ONLY
1936  OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]);
1937  OCMReject([mockVC applicationWillTerminate:[OCMArg any]]);
1938 #else
1939  OCMReject([mockVC sceneWillDisconnect:[OCMArg any]]);
1940  OCMVerify([mockVC applicationWillTerminate:[OCMArg any]]);
1941 #endif
1942  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]);
1943  OCMVerify([mockEngine destroyContext]);
1944  [flutterViewController deregisterNotifications];
1945 }
1946 
1947 - (void)testLifeCycleNotificationDidEnterBackground {
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:UISceneDidEnterBackgroundNotification
1954  object:nil
1955  userInfo:nil];
1956  NSNotification* applicationNotification =
1957  [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
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 sceneDidEnterBackground:[OCMArg any]]);
1965  OCMReject([mockVC applicationDidEnterBackground:[OCMArg any]]);
1966 #else
1967  OCMReject([mockVC sceneDidEnterBackground:[OCMArg any]]);
1968  OCMVerify([mockVC applicationDidEnterBackground:[OCMArg any]]);
1969 #endif
1970  XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1971  OCMVerify([mockVC surfaceUpdated:NO]);
1972  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]);
1973  [flutterViewController deregisterNotifications];
1974 }
1975 
1976 - (void)testLifeCycleNotificationWillEnterForeground {
1977  FlutterEngine* engine = [[FlutterEngine alloc] init];
1978  [engine runWithEntrypoint:nil];
1979  FlutterViewController* flutterViewController =
1980  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1981  NSNotification* sceneNotification =
1982  [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
1983  object:nil
1984  userInfo:nil];
1985  NSNotification* applicationNotification =
1986  [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
1987  object:nil
1988  userInfo:nil];
1989  id mockVC = OCMPartialMock(flutterViewController);
1990  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1991  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1992 #if APPLICATION_EXTENSION_API_ONLY
1993  OCMVerify([mockVC sceneWillEnterForeground:[OCMArg any]]);
1994  OCMReject([mockVC applicationWillEnterForeground:[OCMArg any]]);
1995 #else
1996  OCMReject([mockVC sceneWillEnterForeground:[OCMArg any]]);
1997  OCMVerify([mockVC applicationWillEnterForeground:[OCMArg any]]);
1998 #endif
1999  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2000  [flutterViewController deregisterNotifications];
2001 }
2002 
2003 - (void)testLifeCycleNotificationCancelledInvalidResumed {
2004  FlutterEngine* engine = [[FlutterEngine alloc] init];
2005  [engine runWithEntrypoint:nil];
2006  FlutterViewController* flutterViewController =
2007  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2008  NSNotification* applicationDidBecomeActiveNotification =
2009  [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
2010  object:nil
2011  userInfo:nil];
2012  NSNotification* applicationWillResignActiveNotification =
2013  [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
2014  object:nil
2015  userInfo:nil];
2016  id mockVC = OCMPartialMock(flutterViewController);
2017  [[NSNotificationCenter defaultCenter] postNotification:applicationDidBecomeActiveNotification];
2018  [[NSNotificationCenter defaultCenter] postNotification:applicationWillResignActiveNotification];
2019 #if APPLICATION_EXTENSION_API_ONLY
2020 #else
2021  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2022 #endif
2023 
2024  XCTestExpectation* timeoutApplicationLifeCycle =
2025  [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
2026  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
2027  dispatch_get_main_queue(), ^{
2028  OCMReject([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
2029  [timeoutApplicationLifeCycle fulfill];
2030  [flutterViewController deregisterNotifications];
2031  });
2032  [self waitForExpectationsWithTimeout:5.0 handler:nil];
2033 }
2034 
2035 - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterViewController {
2036  id bundleMock = OCMPartialMock([NSBundle mainBundle]);
2037  OCMStub([bundleMock objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"])
2038  .andReturn(@YES);
2039  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2040  double maxFrameRate = 120;
2041  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2042  FlutterEngine* engine = [[FlutterEngine alloc] init];
2043  [engine runWithEntrypoint:nil];
2044  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2045  nibName:nil
2046  bundle:nil];
2047  FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) {
2048  };
2049  [viewController setUpKeyboardAnimationVsyncClient:callback];
2050  XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2051  CADisplayLink* link = [viewController.keyboardAnimationVSyncClient getDisplayLink];
2052  XCTAssertNotNil(link);
2053  if (@available(iOS 15.0, *)) {
2054  XCTAssertEqual(link.preferredFrameRateRange.maximum, maxFrameRate);
2055  XCTAssertEqual(link.preferredFrameRateRange.preferred, maxFrameRate);
2056  XCTAssertEqual(link.preferredFrameRateRange.minimum, maxFrameRate / 2);
2057  } else {
2058  XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate);
2059  }
2060 }
2061 
2062 - (void)
2063  testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ {
2064  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2065  double maxFrameRate = 120;
2066  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2067  FlutterEngine* engine = [[FlutterEngine alloc] init];
2068  [engine runWithEntrypoint:nil];
2069  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2070  nibName:nil
2071  bundle:nil];
2072  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2073  XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient);
2074 }
2075 
2076 - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists {
2077  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2078  double maxFrameRate = 120;
2079  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2080 
2081  FlutterEngine* engine = [[FlutterEngine alloc] init];
2082  [engine runWithEntrypoint:nil];
2083  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2084  nibName:nil
2085  bundle:nil];
2086  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2087  VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient;
2088  XCTAssertNotNil(clientBefore);
2089 
2090  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2091  VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient;
2092  XCTAssertNotNil(clientAfter);
2093 
2094  XCTAssertTrue(clientBefore == clientAfter);
2095 }
2096 
2097 - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ {
2098  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2099  double maxFrameRate = 60;
2100  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2101  FlutterEngine* engine = [[FlutterEngine alloc] init];
2102  [engine runWithEntrypoint:nil];
2103  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2104  nibName:nil
2105  bundle:nil];
2106  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2107  XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
2108 }
2109 
2110 - (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
2111  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2112  double maxFrameRate = 120;
2113  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2114  FlutterEngine* engine = [[FlutterEngine alloc] init];
2115  [engine runWithEntrypoint:nil];
2116  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2117  nibName:nil
2118  bundle:nil];
2119  [viewController loadView];
2120  [viewController viewDidLoad];
2121 
2122  VSyncClient* client = viewController.touchRateCorrectionVSyncClient;
2123  CADisplayLink* link = [client getDisplayLink];
2124 
2125  UITouch* fakeTouchBegan = [[UITouch alloc] init];
2126  fakeTouchBegan.phase = UITouchPhaseBegan;
2127 
2128  UITouch* fakeTouchMove = [[UITouch alloc] init];
2129  fakeTouchMove.phase = UITouchPhaseMoved;
2130 
2131  UITouch* fakeTouchEnd = [[UITouch alloc] init];
2132  fakeTouchEnd.phase = UITouchPhaseEnded;
2133 
2134  UITouch* fakeTouchCancelled = [[UITouch alloc] init];
2135  fakeTouchCancelled.phase = UITouchPhaseCancelled;
2136 
2137  [viewController
2138  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]];
2139  XCTAssertFalse(link.isPaused);
2140 
2141  [viewController
2142  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]];
2143  XCTAssertTrue(link.isPaused);
2144 
2145  [viewController
2146  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]];
2147  XCTAssertFalse(link.isPaused);
2148 
2149  [viewController
2150  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]];
2151  XCTAssertTrue(link.isPaused);
2152 
2153  [viewController
2154  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2155  initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]];
2156  XCTAssertFalse(link.isPaused);
2157 
2158  [viewController
2159  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd,
2160  fakeTouchCancelled, nil]];
2161  XCTAssertTrue(link.isPaused);
2162 
2163  [viewController
2164  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2165  initWithObjects:fakeTouchMove, fakeTouchEnd, nil]];
2166  XCTAssertFalse(link.isPaused);
2167 }
2168 
2169 - (void)testFlutterViewControllerStartKeyboardAnimationWillCreateVsyncClientCorrectly {
2170  FlutterEngine* engine = [[FlutterEngine alloc] init];
2171  [engine runWithEntrypoint:nil];
2172  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2173  nibName:nil
2174  bundle:nil];
2175  viewController.targetViewInsetBottom = 100;
2176  [viewController startKeyBoardAnimation:0.25];
2177  XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2178 }
2179 
2180 - (void)
2181  testSetupKeyboardAnimationVsyncClientWillNotCreateNewVsyncClientWhenKeyboardAnimationCallbackIsNil {
2182  FlutterEngine* engine = [[FlutterEngine alloc] init];
2183  [engine runWithEntrypoint:nil];
2184  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2185  nibName:nil
2186  bundle:nil];
2187  [viewController setUpKeyboardAnimationVsyncClient:nil];
2188  XCTAssertNil(viewController.keyboardAnimationVSyncClient);
2189 }
2190 
2191 @end
-[FlutterViewController(Tests) invalidateKeyboardAnimationVSyncClient]
void invalidateKeyboardAnimationVSyncClient()
FlutterEnginePartialMock::lifecycleChannel
FlutterBasicMessageChannel * lifecycleChannel
Definition: FlutterKeyboardManagerTest.mm:28
FlutterEnginePartialMock
Sometimes we have to use a custom mock to avoid retain cycles in ocmock.
Definition: FlutterKeyboardManagerTest.mm:27
FlutterEngine
Definition: FlutterEngine.h:61
FlutterBasicMessageChannel
Definition: FlutterChannels.h:37
FlutterViewController
Definition: FlutterViewController.h:56
-[FlutterViewController(Tests) updateViewportMetricsIfNeeded]
void updateViewportMetricsIfNeeded()
-[FlutterEngine runWithEntrypoint:]
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
FlutterViewController(Tests)::targetViewInsetBottom
double targetViewInsetBottom
Definition: FlutterViewControllerTest.mm:123
FlutterTextInputPlugin.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:17
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: FlutterKeyboardManagerTest.mm:40
FlutterFakeKeyEvents.h
-[FlutterEngine(TestLowMemory) notifyLowMemory]
void notifyLowMemory()
flutter
Definition: accessibility_bridge.h:28
-[FlutterViewController(Tests) addInternalPlugins]
void addInternalPlugins()
FlutterBinaryMessenger.h
FlutterTextInputPlugin
Definition: FlutterTextInputPlugin.h:33
FlutterEnginePartialMock::keyEventChannel
FlutterBasicMessageChannel * keyEventChannel
Definition: FlutterViewControllerTest.mm:40
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: FlutterKeyboardManagerTest.mm:52
FlutterEmbedderKeyResponder(Tests)
Definition: FlutterViewControllerTest.mm:117
VSyncClient(Testing)
Definition: FlutterViewControllerTest.mm:182
-[FlutterEngine destroyContext]
void destroyContext()
Definition: FlutterEngine.mm:481
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:243
FlutterDartProject
Definition: FlutterDartProject.mm:262
-[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:27
FLUTTER_ASSERT_ARC
Definition: VsyncWaiterIosTest.mm:15
VSyncClient
Definition: vsync_waiter_ios.h:38
FlutterViewController.h
FlutterViewControllerWillDealloc
const NSNotificationName FlutterViewControllerWillDealloc
Definition: FlutterViewController.mm:43