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