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