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)testAccessibilityPerformEscapePopsRoute {
1369  FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]);
1370  [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil];
1371  id mockNavigationChannel = OCMClassMock([FlutterMethodChannel class]);
1372  OCMStub([mockEngine navigationChannel]).andReturn(mockNavigationChannel);
1373 
1374  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1375  nibName:nil
1376  bundle:nil];
1377  XCTAssertTrue([viewController accessibilityPerformEscape]);
1378 
1379  OCMVerify([mockNavigationChannel invokeMethod:@"popRoute" arguments:nil]);
1380 
1381  [mockNavigationChannel stopMocking];
1382 }
1383 
1384 - (void)testPerformOrientationUpdateForcesOrientationChange {
1385  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1386  currentOrientation:UIInterfaceOrientationLandscapeLeft
1387  didChangeOrientation:YES
1388  resultingOrientation:UIInterfaceOrientationPortrait];
1389 
1390  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1391  currentOrientation:UIInterfaceOrientationLandscapeRight
1392  didChangeOrientation:YES
1393  resultingOrientation:UIInterfaceOrientationPortrait];
1394 
1395  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1396  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1397  didChangeOrientation:YES
1398  resultingOrientation:UIInterfaceOrientationPortrait];
1399 
1400  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1401  currentOrientation:UIInterfaceOrientationLandscapeLeft
1402  didChangeOrientation:YES
1403  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1404 
1405  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1406  currentOrientation:UIInterfaceOrientationLandscapeRight
1407  didChangeOrientation:YES
1408  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1409 
1410  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1411  currentOrientation:UIInterfaceOrientationPortrait
1412  didChangeOrientation:YES
1413  resultingOrientation:UIInterfaceOrientationPortraitUpsideDown];
1414 
1415  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1416  currentOrientation:UIInterfaceOrientationPortrait
1417  didChangeOrientation:YES
1418  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1419 
1420  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1421  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1422  didChangeOrientation:YES
1423  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1424 
1425  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1426  currentOrientation:UIInterfaceOrientationPortrait
1427  didChangeOrientation:YES
1428  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1429 
1430  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1431  currentOrientation:UIInterfaceOrientationLandscapeRight
1432  didChangeOrientation:YES
1433  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1434 
1435  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1436  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1437  didChangeOrientation:YES
1438  resultingOrientation:UIInterfaceOrientationLandscapeLeft];
1439 
1440  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1441  currentOrientation:UIInterfaceOrientationPortrait
1442  didChangeOrientation:YES
1443  resultingOrientation:UIInterfaceOrientationLandscapeRight];
1444 
1445  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1446  currentOrientation:UIInterfaceOrientationLandscapeLeft
1447  didChangeOrientation:YES
1448  resultingOrientation:UIInterfaceOrientationLandscapeRight];
1449 
1450  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1451  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1452  didChangeOrientation:YES
1453  resultingOrientation:UIInterfaceOrientationLandscapeRight];
1454 
1455  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1456  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1457  didChangeOrientation:YES
1458  resultingOrientation:UIInterfaceOrientationPortrait];
1459 }
1460 
1461 - (void)testPerformOrientationUpdateDoesNotForceOrientationChange {
1462  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1463  currentOrientation:UIInterfaceOrientationPortrait
1464  didChangeOrientation:NO
1465  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1466 
1467  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1468  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1469  didChangeOrientation:NO
1470  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1471 
1472  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1473  currentOrientation:UIInterfaceOrientationLandscapeLeft
1474  didChangeOrientation:NO
1475  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1476 
1477  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll
1478  currentOrientation:UIInterfaceOrientationLandscapeRight
1479  didChangeOrientation:NO
1480  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1481 
1482  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1483  currentOrientation:UIInterfaceOrientationPortrait
1484  didChangeOrientation:NO
1485  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1486 
1487  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1488  currentOrientation:UIInterfaceOrientationLandscapeLeft
1489  didChangeOrientation:NO
1490  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1491 
1492  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown
1493  currentOrientation:UIInterfaceOrientationLandscapeRight
1494  didChangeOrientation:NO
1495  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1496 
1497  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait
1498  currentOrientation:UIInterfaceOrientationPortrait
1499  didChangeOrientation:NO
1500  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1501 
1502  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown
1503  currentOrientation:UIInterfaceOrientationPortraitUpsideDown
1504  didChangeOrientation:NO
1505  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1506 
1507  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1508  currentOrientation:UIInterfaceOrientationLandscapeLeft
1509  didChangeOrientation:NO
1510  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1511 
1512  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape
1513  currentOrientation:UIInterfaceOrientationLandscapeRight
1514  didChangeOrientation:NO
1515  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1516 
1517  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft
1518  currentOrientation:UIInterfaceOrientationLandscapeLeft
1519  didChangeOrientation:NO
1520  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1521 
1522  [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight
1523  currentOrientation:UIInterfaceOrientationLandscapeRight
1524  didChangeOrientation:NO
1525  resultingOrientation:static_cast<UIInterfaceOrientation>(0)];
1526 }
1527 
1528 // Perform an orientation update test that fails when the expected outcome
1529 // for an orientation update is not met
1530 - (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask
1531  currentOrientation:(UIInterfaceOrientation)currentOrientation
1532  didChangeOrientation:(BOOL)didChange
1533  resultingOrientation:(UIInterfaceOrientation)resultingOrientation {
1534  id mockApplication = OCMClassMock([UIApplication class]);
1535  id mockWindowScene;
1536  id deviceMock;
1537  id mockVC;
1538  __block __weak id weakPreferences;
1539  @autoreleasepool {
1540  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1541  nibName:nil
1542  bundle:nil];
1543 
1544  if (@available(iOS 16.0, *)) {
1545  mockWindowScene = OCMClassMock([UIWindowScene class]);
1546  mockVC = OCMPartialMock(realVC);
1547  OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1548  if (realVC.supportedInterfaceOrientations == mask) {
1549  OCMReject([mockWindowScene requestGeometryUpdateWithPreferences:[OCMArg any]
1550  errorHandler:[OCMArg any]]);
1551  } else {
1552  // iOS 16 will decide whether to rotate based on the new preference, so always set it
1553  // when it changes.
1554  OCMExpect([mockWindowScene
1555  requestGeometryUpdateWithPreferences:[OCMArg checkWithBlock:^BOOL(
1556  UIWindowSceneGeometryPreferencesIOS*
1557  preferences) {
1558  weakPreferences = preferences;
1559  return preferences.interfaceOrientations == mask;
1560  }]
1561  errorHandler:[OCMArg any]]);
1562  }
1563  OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
1564  OCMStub([mockApplication connectedScenes]).andReturn([NSSet setWithObject:mockWindowScene]);
1565  } else {
1566  deviceMock = OCMPartialMock([UIDevice currentDevice]);
1567  if (!didChange) {
1568  OCMReject([deviceMock setValue:[OCMArg any] forKey:@"orientation"]);
1569  } else {
1570  OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]);
1571  }
1572  if (@available(iOS 13.0, *)) {
1573  mockWindowScene = OCMClassMock([UIWindowScene class]);
1574  mockVC = OCMPartialMock(realVC);
1575  OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene);
1576  OCMStub(((UIWindowScene*)mockWindowScene).interfaceOrientation)
1577  .andReturn(currentOrientation);
1578  } else {
1579  OCMStub([mockApplication sharedApplication]).andReturn(mockApplication);
1580  OCMStub([mockApplication statusBarOrientation]).andReturn(currentOrientation);
1581  }
1582  }
1583 
1584  [realVC performOrientationUpdate:mask];
1585  if (@available(iOS 16.0, *)) {
1586  OCMVerifyAll(mockWindowScene);
1587  } else {
1588  OCMVerifyAll(deviceMock);
1589  }
1590  }
1591  [mockWindowScene stopMocking];
1592  [deviceMock stopMocking];
1593  [mockApplication stopMocking];
1594  XCTAssertNil(weakPreferences);
1595 }
1596 
1597 // Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast,
1598 // which is set to the given "contrast".
1599 - (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast {
1600  id mockTraitCollection = OCMClassMock([UITraitCollection class]);
1601  OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast);
1602  return mockTraitCollection;
1603 }
1604 
1605 - (void)testWillDeallocNotification {
1606  XCTestExpectation* expectation =
1607  [[XCTestExpectation alloc] initWithDescription:@"notification called"];
1608  id engine = [[MockEngine alloc] init];
1609  @autoreleasepool {
1610  // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores)
1611  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1612  nibName:nil
1613  bundle:nil];
1614  [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc
1615  object:nil
1616  queue:[NSOperationQueue mainQueue]
1617  usingBlock:^(NSNotification* _Nonnull note) {
1618  [expectation fulfill];
1619  }];
1620  XCTAssertNotNil(realVC);
1621  realVC = nil;
1622  }
1623  [self waitForExpectations:@[ expectation ] timeout:1.0];
1624 }
1625 
1626 - (void)testReleasesKeyboardManagerOnDealloc {
1627  __weak FlutterKeyboardManager* weakKeyboardManager = nil;
1628  @autoreleasepool {
1630 
1631  [viewController addInternalPlugins];
1632  weakKeyboardManager = viewController.keyboardManager;
1633  XCTAssertNotNil(weakKeyboardManager);
1634  [viewController deregisterNotifications];
1635  viewController = nil;
1636  }
1637  // View controller has released the keyboard manager.
1638  XCTAssertNil(weakKeyboardManager);
1639 }
1640 
1641 - (void)testDoesntLoadViewInInit {
1642  FlutterDartProject* project = [[FlutterDartProject alloc] init];
1643  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1644  [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1645  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1646  nibName:nil
1647  bundle:nil];
1648  XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown");
1649  engine.viewController = nil;
1650 }
1651 
1652 - (void)testHideOverlay {
1653  FlutterDartProject* project = [[FlutterDartProject alloc] init];
1654  FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project];
1655  [engine createShell:@"" libraryURI:@"" initialRoute:nil];
1656  FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine
1657  nibName:nil
1658  bundle:nil];
1659  XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @"");
1660  [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator
1661  object:nil];
1662  XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @"");
1663  engine.viewController = nil;
1664 }
1665 
1666 - (void)testNotifyLowMemory {
1668  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine
1669  nibName:nil
1670  bundle:nil];
1671  id viewControllerMock = OCMPartialMock(viewController);
1672  OCMStub([viewControllerMock surfaceUpdated:NO]);
1673  [viewController beginAppearanceTransition:NO animated:NO];
1674  [viewController endAppearanceTransition];
1675  XCTAssertTrue(mockEngine.didCallNotifyLowMemory);
1676 }
1677 
1678 - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback {
1679  NSMutableDictionary* replyMessage = [@{
1680  @"handled" : @YES,
1681  } mutableCopy];
1682  // Response is async, so we have to post it to the run loop instead of calling
1683  // it directly.
1684  self.messageSent = message;
1685  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode,
1686  ^() {
1687  callback(replyMessage);
1688  });
1689 }
1690 
1691 - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) {
1692  if (@available(iOS 13.4, *)) {
1693  // noop
1694  } else {
1695  return;
1696  }
1698  mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1699  OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1700  .andCall(self, @selector(sendMessage:reply:));
1701  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1702  mockEngine.textInputPlugin = self.mockTextInputPlugin;
1703 
1704  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1705  nibName:nil
1706  bundle:nil];
1707 
1708  // Allocate the keyboard manager in the view controller by adding the internal
1709  // plugins.
1710  [vc addInternalPlugins];
1711 
1712  [vc handlePressEvent:keyUpEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0)
1713  nextAction:^(){
1714  }];
1715 
1716  XCTAssert(self.messageSent != nil);
1717  XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1718  XCTAssert([self.messageSent[@"type"] isEqualToString:@"keyup"]);
1719  XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1720  XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1721  XCTAssert([self.messageSent[@"characters"] isEqualToString:@""]);
1722  XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@""]);
1723  [vc deregisterNotifications];
1724 }
1725 
1726 - (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) {
1727  if (@available(iOS 13.4, *)) {
1728  // noop
1729  } else {
1730  return;
1731  }
1732 
1734  mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1735  OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1736  .andCall(self, @selector(sendMessage:reply:));
1737  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1738  mockEngine.textInputPlugin = self.mockTextInputPlugin;
1739 
1740  __strong FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine
1741  nibName:nil
1742  bundle:nil];
1743  // Allocate the keyboard manager in the view controller by adding the internal
1744  // plugins.
1745  [vc addInternalPlugins];
1746 
1747  [vc handlePressEvent:keyDownEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0f, "A",
1748  "a")
1749  nextAction:^(){
1750  }];
1751 
1752  XCTAssert(self.messageSent != nil);
1753  XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]);
1754  XCTAssert([self.messageSent[@"type"] isEqualToString:@"keydown"]);
1755  XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]);
1756  XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]);
1757  XCTAssert([self.messageSent[@"characters"] isEqualToString:@"A"]);
1758  XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@"a"]);
1759  [vc deregisterNotifications];
1760  vc = nil;
1761 }
1762 
1763 - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) {
1764  if (@available(iOS 13.4, *)) {
1765  // noop
1766  } else {
1767  return;
1768  }
1769  id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]);
1770  OCMStub([keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]])
1771  .andCall(self, @selector(sendMessage:reply:));
1772  OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES);
1773  OCMStub([self.mockEngine keyEventChannel]).andReturn(keyEventChannel);
1774 
1775  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1776  nibName:nil
1777  bundle:nil];
1778 
1779  // Allocate the keyboard manager in the view controller by adding the internal
1780  // plugins.
1781  [vc addInternalPlugins];
1782 
1783  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseStationary, UIKeyboardHIDUsageKeyboardA,
1784  UIKeyModifierShift, 123.0)
1785  nextAction:^(){
1786  }];
1787  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseCancelled, UIKeyboardHIDUsageKeyboardA,
1788  UIKeyModifierShift, 123.0)
1789  nextAction:^(){
1790  }];
1791  [vc handlePressEvent:keyEventWithPhase(UIPressPhaseChanged, UIKeyboardHIDUsageKeyboardA,
1792  UIKeyModifierShift, 123.0)
1793  nextAction:^(){
1794  }];
1795 
1796  XCTAssert(self.messageSent == nil);
1797  OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]);
1798  [vc deregisterNotifications];
1799 }
1800 
1801 - (void)testPanGestureRecognizer API_AVAILABLE(ios(13.4)) {
1802  if (@available(iOS 13.4, *)) {
1803  // noop
1804  } else {
1805  return;
1806  }
1807 
1808  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1809  nibName:nil
1810  bundle:nil];
1811  XCTAssertNotNil(vc);
1812  UIView* view = vc.view;
1813  XCTAssertNotNil(view);
1814  NSArray* gestureRecognizers = view.gestureRecognizers;
1815  XCTAssertNotNil(gestureRecognizers);
1816 
1817  BOOL found = NO;
1818  for (id gesture in gestureRecognizers) {
1819  if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) {
1820  found = YES;
1821  break;
1822  }
1823  }
1824  XCTAssertTrue(found);
1825 }
1826 
1827 - (void)testMouseSupport API_AVAILABLE(ios(13.4)) {
1828  if (@available(iOS 13.4, *)) {
1829  // noop
1830  } else {
1831  return;
1832  }
1833 
1834  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1835  nibName:nil
1836  bundle:nil];
1837  XCTAssertNotNil(vc);
1838 
1839  id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]);
1840  XCTAssertNotNil(mockPanGestureRecognizer);
1841 
1842  [vc discreteScrollEvent:mockPanGestureRecognizer];
1843 
1844  [[[self.mockEngine verify] ignoringNonObjectArgs]
1845  dispatchPointerDataPacket:std::make_unique<flutter::PointerDataPacket>(0)];
1846 }
1847 
1848 - (void)testFakeEventTimeStamp {
1849  FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine
1850  nibName:nil
1851  bundle:nil];
1852  XCTAssertNotNil(vc);
1853 
1854  flutter::PointerData pointer_data = [vc generatePointerDataForFake];
1855  int64_t current_micros = [[NSProcessInfo processInfo] systemUptime] * 1000 * 1000;
1856  int64_t interval_micros = current_micros - pointer_data.time_stamp;
1857  const int64_t tolerance_millis = 2;
1858  XCTAssertTrue(interval_micros / 1000 < tolerance_millis,
1859  @"PointerData.time_stamp should be equal to NSProcessInfo.systemUptime");
1860 }
1861 
1862 - (void)testSplashScreenViewCanSetNil {
1863  FlutterViewController* flutterViewController =
1864  [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil];
1865  [flutterViewController setSplashScreenView:nil];
1866 }
1867 
1868 - (void)testLifeCycleNotificationBecameActive {
1869  FlutterEngine* engine = [[FlutterEngine alloc] init];
1870  [engine runWithEntrypoint:nil];
1871  FlutterViewController* flutterViewController =
1872  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1873  UIWindow* window = [[UIWindow alloc] init];
1874  [window addSubview:flutterViewController.view];
1875  flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100);
1876  [flutterViewController viewDidLayoutSubviews];
1877  NSNotification* sceneNotification =
1878  [NSNotification notificationWithName:UISceneDidActivateNotification object:nil userInfo:nil];
1879  NSNotification* applicationNotification =
1880  [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
1881  object:nil
1882  userInfo:nil];
1883  id mockVC = OCMPartialMock(flutterViewController);
1884  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1885  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1886 #if APPLICATION_EXTENSION_API_ONLY
1887  OCMVerify([mockVC sceneBecameActive:[OCMArg any]]);
1888  OCMReject([mockVC applicationBecameActive:[OCMArg any]]);
1889 #else
1890  OCMReject([mockVC sceneBecameActive:[OCMArg any]]);
1891  OCMVerify([mockVC applicationBecameActive:[OCMArg any]]);
1892 #endif
1893  XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1894  OCMVerify([mockVC surfaceUpdated:YES]);
1895  XCTestExpectation* timeoutApplicationLifeCycle =
1896  [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
1897  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
1898  dispatch_get_main_queue(), ^{
1899  [timeoutApplicationLifeCycle fulfill];
1900  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
1901  [flutterViewController deregisterNotifications];
1902  });
1903  [self waitForExpectationsWithTimeout:5.0 handler:nil];
1904 }
1905 
1906 - (void)testLifeCycleNotificationWillResignActive {
1907  FlutterEngine* engine = [[FlutterEngine alloc] init];
1908  [engine runWithEntrypoint:nil];
1909  FlutterViewController* flutterViewController =
1910  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1911  NSNotification* sceneNotification =
1912  [NSNotification notificationWithName:UISceneWillDeactivateNotification
1913  object:nil
1914  userInfo:nil];
1915  NSNotification* applicationNotification =
1916  [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
1917  object:nil
1918  userInfo:nil];
1919  id mockVC = OCMPartialMock(flutterViewController);
1920  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1921  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1922 #if APPLICATION_EXTENSION_API_ONLY
1923  OCMVerify([mockVC sceneWillResignActive:[OCMArg any]]);
1924  OCMReject([mockVC applicationWillResignActive:[OCMArg any]]);
1925 #else
1926  OCMReject([mockVC sceneWillResignActive:[OCMArg any]]);
1927  OCMVerify([mockVC applicationWillResignActive:[OCMArg any]]);
1928 #endif
1929  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
1930  [flutterViewController deregisterNotifications];
1931 }
1932 
1933 - (void)testLifeCycleNotificationWillTerminate {
1934  FlutterEngine* engine = [[FlutterEngine alloc] init];
1935  [engine runWithEntrypoint:nil];
1936  FlutterViewController* flutterViewController =
1937  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1938  NSNotification* sceneNotification =
1939  [NSNotification notificationWithName:UISceneDidDisconnectNotification
1940  object:nil
1941  userInfo:nil];
1942  NSNotification* applicationNotification =
1943  [NSNotification notificationWithName:UIApplicationWillTerminateNotification
1944  object:nil
1945  userInfo:nil];
1946  id mockVC = OCMPartialMock(flutterViewController);
1947  id mockEngine = OCMPartialMock(engine);
1948  OCMStub([mockVC engine]).andReturn(mockEngine);
1949  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1950  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1951 #if APPLICATION_EXTENSION_API_ONLY
1952  OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]);
1953  OCMReject([mockVC applicationWillTerminate:[OCMArg any]]);
1954 #else
1955  OCMReject([mockVC sceneWillDisconnect:[OCMArg any]]);
1956  OCMVerify([mockVC applicationWillTerminate:[OCMArg any]]);
1957 #endif
1958  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]);
1959  OCMVerify([mockEngine destroyContext]);
1960  [flutterViewController deregisterNotifications];
1961 }
1962 
1963 - (void)testLifeCycleNotificationDidEnterBackground {
1964  FlutterEngine* engine = [[FlutterEngine alloc] init];
1965  [engine runWithEntrypoint:nil];
1966  FlutterViewController* flutterViewController =
1967  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1968  NSNotification* sceneNotification =
1969  [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification
1970  object:nil
1971  userInfo:nil];
1972  NSNotification* applicationNotification =
1973  [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification
1974  object:nil
1975  userInfo:nil];
1976  id mockVC = OCMPartialMock(flutterViewController);
1977  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
1978  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
1979 #if APPLICATION_EXTENSION_API_ONLY
1980  OCMVerify([mockVC sceneDidEnterBackground:[OCMArg any]]);
1981  OCMReject([mockVC applicationDidEnterBackground:[OCMArg any]]);
1982 #else
1983  OCMReject([mockVC sceneDidEnterBackground:[OCMArg any]]);
1984  OCMVerify([mockVC applicationDidEnterBackground:[OCMArg any]]);
1985 #endif
1986  XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground);
1987  OCMVerify([mockVC surfaceUpdated:NO]);
1988  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]);
1989  [flutterViewController deregisterNotifications];
1990 }
1991 
1992 - (void)testLifeCycleNotificationWillEnterForeground {
1993  FlutterEngine* engine = [[FlutterEngine alloc] init];
1994  [engine runWithEntrypoint:nil];
1995  FlutterViewController* flutterViewController =
1996  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
1997  NSNotification* sceneNotification =
1998  [NSNotification notificationWithName:UISceneWillEnterForegroundNotification
1999  object:nil
2000  userInfo:nil];
2001  NSNotification* applicationNotification =
2002  [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification
2003  object:nil
2004  userInfo:nil];
2005  id mockVC = OCMPartialMock(flutterViewController);
2006  [[NSNotificationCenter defaultCenter] postNotification:sceneNotification];
2007  [[NSNotificationCenter defaultCenter] postNotification:applicationNotification];
2008 #if APPLICATION_EXTENSION_API_ONLY
2009  OCMVerify([mockVC sceneWillEnterForeground:[OCMArg any]]);
2010  OCMReject([mockVC applicationWillEnterForeground:[OCMArg any]]);
2011 #else
2012  OCMReject([mockVC sceneWillEnterForeground:[OCMArg any]]);
2013  OCMVerify([mockVC applicationWillEnterForeground:[OCMArg any]]);
2014 #endif
2015  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2016  [flutterViewController deregisterNotifications];
2017 }
2018 
2019 - (void)testLifeCycleNotificationCancelledInvalidResumed {
2020  FlutterEngine* engine = [[FlutterEngine alloc] init];
2021  [engine runWithEntrypoint:nil];
2022  FlutterViewController* flutterViewController =
2023  [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil];
2024  NSNotification* applicationDidBecomeActiveNotification =
2025  [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification
2026  object:nil
2027  userInfo:nil];
2028  NSNotification* applicationWillResignActiveNotification =
2029  [NSNotification notificationWithName:UIApplicationWillResignActiveNotification
2030  object:nil
2031  userInfo:nil];
2032  id mockVC = OCMPartialMock(flutterViewController);
2033  [[NSNotificationCenter defaultCenter] postNotification:applicationDidBecomeActiveNotification];
2034  [[NSNotificationCenter defaultCenter] postNotification:applicationWillResignActiveNotification];
2035 #if APPLICATION_EXTENSION_API_ONLY
2036 #else
2037  OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]);
2038 #endif
2039 
2040  XCTestExpectation* timeoutApplicationLifeCycle =
2041  [self expectationWithDescription:@"timeoutApplicationLifeCycle"];
2042  dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)),
2043  dispatch_get_main_queue(), ^{
2044  OCMReject([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]);
2045  [timeoutApplicationLifeCycle fulfill];
2046  [flutterViewController deregisterNotifications];
2047  });
2048  [self waitForExpectationsWithTimeout:5.0 handler:nil];
2049 }
2050 
2051 - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterViewController {
2052  id bundleMock = OCMPartialMock([NSBundle mainBundle]);
2053  OCMStub([bundleMock objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"])
2054  .andReturn(@YES);
2055  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2056  double maxFrameRate = 120;
2057  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2058  FlutterEngine* engine = [[FlutterEngine alloc] init];
2059  [engine runWithEntrypoint:nil];
2060  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2061  nibName:nil
2062  bundle:nil];
2063  FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) {
2064  };
2065  [viewController setUpKeyboardAnimationVsyncClient:callback];
2066  XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2067  CADisplayLink* link = [viewController.keyboardAnimationVSyncClient getDisplayLink];
2068  XCTAssertNotNil(link);
2069  if (@available(iOS 15.0, *)) {
2070  XCTAssertEqual(link.preferredFrameRateRange.maximum, maxFrameRate);
2071  XCTAssertEqual(link.preferredFrameRateRange.preferred, maxFrameRate);
2072  XCTAssertEqual(link.preferredFrameRateRange.minimum, maxFrameRate / 2);
2073  } else {
2074  XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate);
2075  }
2076 }
2077 
2078 - (void)
2079  testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ {
2080  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2081  double maxFrameRate = 120;
2082  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2083  FlutterEngine* engine = [[FlutterEngine alloc] init];
2084  [engine runWithEntrypoint:nil];
2085  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2086  nibName:nil
2087  bundle:nil];
2088  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2089  XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient);
2090 }
2091 
2092 - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists {
2093  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2094  double maxFrameRate = 120;
2095  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2096 
2097  FlutterEngine* engine = [[FlutterEngine alloc] init];
2098  [engine runWithEntrypoint:nil];
2099  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2100  nibName:nil
2101  bundle:nil];
2102  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2103  VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient;
2104  XCTAssertNotNil(clientBefore);
2105 
2106  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2107  VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient;
2108  XCTAssertNotNil(clientAfter);
2109 
2110  XCTAssertTrue(clientBefore == clientAfter);
2111 }
2112 
2113 - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ {
2114  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2115  double maxFrameRate = 60;
2116  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2117  FlutterEngine* engine = [[FlutterEngine alloc] init];
2118  [engine runWithEntrypoint:nil];
2119  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2120  nibName:nil
2121  bundle:nil];
2122  [viewController createTouchRateCorrectionVSyncClientIfNeeded];
2123  XCTAssertNil(viewController.touchRateCorrectionVSyncClient);
2124 }
2125 
2126 - (void)testTriggerTouchRateCorrectionVSyncClientCorrectly {
2127  id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]];
2128  double maxFrameRate = 120;
2129  [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate];
2130  FlutterEngine* engine = [[FlutterEngine alloc] init];
2131  [engine runWithEntrypoint:nil];
2132  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2133  nibName:nil
2134  bundle:nil];
2135  [viewController loadView];
2136  [viewController viewDidLoad];
2137 
2138  VSyncClient* client = viewController.touchRateCorrectionVSyncClient;
2139  CADisplayLink* link = [client getDisplayLink];
2140 
2141  UITouch* fakeTouchBegan = [[UITouch alloc] init];
2142  fakeTouchBegan.phase = UITouchPhaseBegan;
2143 
2144  UITouch* fakeTouchMove = [[UITouch alloc] init];
2145  fakeTouchMove.phase = UITouchPhaseMoved;
2146 
2147  UITouch* fakeTouchEnd = [[UITouch alloc] init];
2148  fakeTouchEnd.phase = UITouchPhaseEnded;
2149 
2150  UITouch* fakeTouchCancelled = [[UITouch alloc] init];
2151  fakeTouchCancelled.phase = UITouchPhaseCancelled;
2152 
2153  [viewController
2154  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]];
2155  XCTAssertFalse(link.isPaused);
2156 
2157  [viewController
2158  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]];
2159  XCTAssertTrue(link.isPaused);
2160 
2161  [viewController
2162  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]];
2163  XCTAssertFalse(link.isPaused);
2164 
2165  [viewController
2166  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]];
2167  XCTAssertTrue(link.isPaused);
2168 
2169  [viewController
2170  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2171  initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]];
2172  XCTAssertFalse(link.isPaused);
2173 
2174  [viewController
2175  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd,
2176  fakeTouchCancelled, nil]];
2177  XCTAssertTrue(link.isPaused);
2178 
2179  [viewController
2180  triggerTouchRateCorrectionIfNeeded:[[NSSet alloc]
2181  initWithObjects:fakeTouchMove, fakeTouchEnd, nil]];
2182  XCTAssertFalse(link.isPaused);
2183 }
2184 
2185 - (void)testFlutterViewControllerStartKeyboardAnimationWillCreateVsyncClientCorrectly {
2186  FlutterEngine* engine = [[FlutterEngine alloc] init];
2187  [engine runWithEntrypoint:nil];
2188  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2189  nibName:nil
2190  bundle:nil];
2191  viewController.targetViewInsetBottom = 100;
2192  [viewController startKeyBoardAnimation:0.25];
2193  XCTAssertNotNil(viewController.keyboardAnimationVSyncClient);
2194 }
2195 
2196 - (void)
2197  testSetupKeyboardAnimationVsyncClientWillNotCreateNewVsyncClientWhenKeyboardAnimationCallbackIsNil {
2198  FlutterEngine* engine = [[FlutterEngine alloc] init];
2199  [engine runWithEntrypoint:nil];
2200  FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine
2201  nibName:nil
2202  bundle:nil];
2203  [viewController setUpKeyboardAnimationVsyncClient:nil];
2204  XCTAssertNil(viewController.keyboardAnimationVSyncClient);
2205 }
2206 
2207 @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
FlutterMethodChannel
Definition: FlutterChannels.h:220
-[FlutterViewController(Tests) updateViewportMetricsIfNeeded]
void updateViewportMetricsIfNeeded()
-[FlutterEngine runWithEntrypoint:]
BOOL runWithEntrypoint:(nullable NSString *entrypoint)
FlutterViewController(Tests)::targetViewInsetBottom
double targetViewInsetBottom
Definition: FlutterViewControllerTest.mm:123
FlutterTextInputPlugin.h
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:264
-[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