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