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