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