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