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