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