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