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