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