| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #import <OCMock/OCMock.h> |
| #import <XCTest/XCTest.h> |
| |
| #include "flutter/fml/platform/darwin/message_loop_darwin.h" |
| #import "flutter/lib/ui/window/platform_configuration.h" |
| #include "flutter/lib/ui/window/pointer_data.h" |
| #import "flutter/lib/ui/window/viewport_metrics.h" |
| #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterBinaryMessenger.h" |
| #import "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterEmbedderKeyResponder.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterFakeKeyEvents.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterTextInputPlugin.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Source/FlutterViewController_Internal.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Source/UIViewController+FlutterScreenAndSceneIfLoaded.h" |
| #import "flutter/shell/platform/darwin/ios/framework/Source/vsync_waiter_ios.h" |
| #import "flutter/shell/platform/embedder/embedder.h" |
| #import "flutter/third_party/spring_animation/spring_animation.h" |
| |
| FLUTTER_ASSERT_ARC |
| |
| using namespace flutter::testing; |
| |
| @interface FlutterEngine () |
| - (FlutterTextInputPlugin*)textInputPlugin; |
| - (void)sendKeyEvent:(const FlutterKeyEvent&)event |
| callback:(nullable FlutterKeyEventCallback)callback |
| userData:(nullable void*)userData; |
| - (fml::RefPtr<fml::TaskRunner>)uiTaskRunner; |
| @end |
| |
| /// Sometimes we have to use a custom mock to avoid retain cycles in OCMock. |
| /// Used for testing low memory notification. |
| @interface FlutterEnginePartialMock : FlutterEngine |
| @property(nonatomic, strong) FlutterBasicMessageChannel* lifecycleChannel; |
| @property(nonatomic, strong) FlutterBasicMessageChannel* keyEventChannel; |
| @property(nonatomic, weak) FlutterViewController* viewController; |
| @property(nonatomic, strong) FlutterTextInputPlugin* textInputPlugin; |
| @property(nonatomic, assign) BOOL didCallNotifyLowMemory; |
| - (FlutterTextInputPlugin*)textInputPlugin; |
| - (void)sendKeyEvent:(const FlutterKeyEvent&)event |
| callback:(nullable FlutterKeyEventCallback)callback |
| userData:(nullable void*)userData; |
| @end |
| |
| @implementation FlutterEnginePartialMock |
| @synthesize viewController; |
| @synthesize lifecycleChannel; |
| @synthesize keyEventChannel; |
| @synthesize textInputPlugin; |
| |
| - (void)notifyLowMemory { |
| _didCallNotifyLowMemory = YES; |
| } |
| |
| - (void)sendKeyEvent:(const FlutterKeyEvent&)event |
| callback:(FlutterKeyEventCallback)callback |
| userData:(void*)userData API_AVAILABLE(ios(9.0)) { |
| if (callback == nil) { |
| return; |
| } |
| // NSAssert(callback != nullptr, @"Invalid callback"); |
| // Response is async, so we have to post it to the run loop instead of calling |
| // it directly. |
| CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, |
| ^() { |
| callback(true, userData); |
| }); |
| } |
| @end |
| |
| @interface FlutterEngine () |
| - (BOOL)createShell:(NSString*)entrypoint |
| libraryURI:(NSString*)libraryURI |
| initialRoute:(NSString*)initialRoute; |
| - (void)dispatchPointerDataPacket:(std::unique_ptr<flutter::PointerDataPacket>)packet; |
| - (void)updateViewportMetrics:(flutter::ViewportMetrics)viewportMetrics; |
| - (void)attachView; |
| @end |
| |
| @interface FlutterEngine (TestLowMemory) |
| - (void)notifyLowMemory; |
| @end |
| |
| extern NSNotificationName const FlutterViewControllerWillDealloc; |
| |
| /// A simple mock class for FlutterEngine. |
| /// |
| /// OCMClassMock can't be used for FlutterEngine sometimes because OCMock retains arguments to |
| /// invocations and since the init for FlutterViewController calls a method on the |
| /// FlutterEngine it creates a retain cycle that stops us from testing behaviors related to |
| /// deleting FlutterViewControllers. |
| /// |
| /// Used for testing deallocation. |
| @interface MockEngine : NSObject |
| @property(nonatomic, strong) FlutterDartProject* project; |
| @end |
| |
| @implementation MockEngine |
| - (FlutterViewController*)viewController { |
| return nil; |
| } |
| - (void)setViewController:(FlutterViewController*)viewController { |
| // noop |
| } |
| @end |
| |
| @interface FlutterKeyboardManager (Tests) |
| @property(nonatomic, retain, readonly) |
| NSMutableArray<id<FlutterKeyPrimaryResponder>>* primaryResponders; |
| @end |
| |
| @interface FlutterEmbedderKeyResponder (Tests) |
| @property(nonatomic, copy, readonly) FlutterSendKeyEvent sendEvent; |
| @end |
| |
| @interface FlutterViewController (Tests) |
| |
| @property(nonatomic, assign) double targetViewInsetBottom; |
| @property(nonatomic, assign) BOOL isKeyboardInOrTransitioningFromBackground; |
| @property(nonatomic, assign) BOOL keyboardAnimationIsShowing; |
| @property(nonatomic, strong) VSyncClient* keyboardAnimationVSyncClient; |
| @property(nonatomic, strong) VSyncClient* touchRateCorrectionVSyncClient; |
| |
| - (void)createTouchRateCorrectionVSyncClientIfNeeded; |
| - (void)surfaceUpdated:(BOOL)appeared; |
| - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences; |
| - (void)handlePressEvent:(FlutterUIPressProxy*)press |
| nextAction:(void (^)())next API_AVAILABLE(ios(13.4)); |
| - (void)discreteScrollEvent:(UIPanGestureRecognizer*)recognizer; |
| - (void)updateViewportMetricsIfNeeded; |
| - (void)onUserSettingsChanged:(NSNotification*)notification; |
| - (void)applicationWillTerminate:(NSNotification*)notification; |
| - (void)goToApplicationLifecycle:(nonnull NSString*)state; |
| - (void)handleKeyboardNotification:(NSNotification*)notification; |
| - (CGFloat)calculateKeyboardInset:(CGRect)keyboardFrame keyboardMode:(int)keyboardMode; |
| - (BOOL)shouldIgnoreKeyboardNotification:(NSNotification*)notification; |
| - (FlutterKeyboardMode)calculateKeyboardAttachMode:(NSNotification*)notification; |
| - (CGFloat)calculateMultitaskingAdjustment:(CGRect)screenRect keyboardFrame:(CGRect)keyboardFrame; |
| - (void)startKeyBoardAnimation:(NSTimeInterval)duration; |
| - (UIView*)keyboardAnimationView; |
| - (SpringAnimation*)keyboardSpringAnimation; |
| - (void)setUpKeyboardSpringAnimationIfNeeded:(CAAnimation*)keyboardAnimation; |
| - (void)setUpKeyboardAnimationVsyncClient: |
| (FlutterKeyboardAnimationCallback)keyboardAnimationCallback; |
| - (void)ensureViewportMetricsIsCorrect; |
| - (void)invalidateKeyboardAnimationVSyncClient; |
| - (void)addInternalPlugins; |
| - (flutter::PointerData)generatePointerDataForFake; |
| - (void)sharedSetupWithProject:(nullable FlutterDartProject*)project |
| initialRoute:(nullable NSString*)initialRoute; |
| - (void)applicationBecameActive:(NSNotification*)notification; |
| - (void)applicationWillResignActive:(NSNotification*)notification; |
| - (void)applicationWillTerminate:(NSNotification*)notification; |
| - (void)applicationDidEnterBackground:(NSNotification*)notification; |
| - (void)applicationWillEnterForeground:(NSNotification*)notification; |
| - (void)sceneBecameActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)); |
| - (void)sceneWillResignActive:(NSNotification*)notification API_AVAILABLE(ios(13.0)); |
| - (void)sceneWillDisconnect:(NSNotification*)notification API_AVAILABLE(ios(13.0)); |
| - (void)sceneDidEnterBackground:(NSNotification*)notification API_AVAILABLE(ios(13.0)); |
| - (void)sceneWillEnterForeground:(NSNotification*)notification API_AVAILABLE(ios(13.0)); |
| - (void)triggerTouchRateCorrectionIfNeeded:(NSSet*)touches; |
| @end |
| |
| @interface FlutterViewControllerTest : XCTestCase |
| @property(nonatomic, strong) id mockEngine; |
| @property(nonatomic, strong) id mockTextInputPlugin; |
| @property(nonatomic, strong) id messageSent; |
| - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback; |
| @end |
| |
| @interface UITouch () |
| |
| @property(nonatomic, readwrite) UITouchPhase phase; |
| |
| @end |
| |
| @interface VSyncClient (Testing) |
| |
| - (CADisplayLink*)getDisplayLink; |
| |
| @end |
| |
| @implementation FlutterViewControllerTest |
| |
| - (void)setUp { |
| self.mockEngine = OCMClassMock([FlutterEngine class]); |
| self.mockTextInputPlugin = OCMClassMock([FlutterTextInputPlugin class]); |
| OCMStub([self.mockEngine textInputPlugin]).andReturn(self.mockTextInputPlugin); |
| self.messageSent = nil; |
| } |
| |
| - (void)tearDown { |
| // We stop mocking here to avoid retain cycles that stop |
| // FlutterViewControllers from deallocing. |
| [self.mockEngine stopMocking]; |
| self.mockEngine = nil; |
| self.mockTextInputPlugin = nil; |
| self.messageSent = nil; |
| } |
| |
| - (id)setUpMockScreen { |
| UIScreen* mockScreen = OCMClassMock([UIScreen class]); |
| // iPhone 14 pixels |
| CGRect screenBounds = CGRectMake(0, 0, 1170, 2532); |
| OCMStub([mockScreen bounds]).andReturn(screenBounds); |
| CGFloat screenScale = 1; |
| OCMStub([mockScreen scale]).andReturn(screenScale); |
| |
| return mockScreen; |
| } |
| |
| - (id)setUpMockView:(FlutterViewController*)viewControllerMock |
| screen:(UIScreen*)screen |
| viewFrame:(CGRect)viewFrame |
| convertedFrame:(CGRect)convertedFrame { |
| OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen); |
| id mockView = OCMClassMock([UIView class]); |
| OCMStub([mockView frame]).andReturn(viewFrame); |
| OCMStub([mockView convertRect:viewFrame toCoordinateSpace:[OCMArg any]]) |
| .andReturn(convertedFrame); |
| OCMStub([viewControllerMock viewIfLoaded]).andReturn(mockView); |
| |
| return mockView; |
| } |
| |
| - (void)testViewDidLoadWillInvokeCreateTouchRateCorrectionVSyncClient { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| [viewControllerMock loadView]; |
| [viewControllerMock viewDidLoad]; |
| OCMVerify([viewControllerMock createTouchRateCorrectionVSyncClientIfNeeded]); |
| } |
| |
| - (void)testStartKeyboardAnimationWillInvokeSetupKeyboardSpringAnimationIfNeeded { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| viewControllerMock.targetViewInsetBottom = 100; |
| [viewControllerMock startKeyBoardAnimation:0.25]; |
| |
| CAAnimation* keyboardAnimation = |
| [[viewControllerMock keyboardAnimationView].layer animationForKey:@"position"]; |
| |
| OCMVerify([viewControllerMock setUpKeyboardSpringAnimationIfNeeded:keyboardAnimation]); |
| } |
| |
| - (void)testSetupKeyboardSpringAnimationIfNeeded { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| CGRect viewFrame = screen.bounds; |
| [self setUpMockView:viewControllerMock |
| screen:screen |
| viewFrame:viewFrame |
| convertedFrame:viewFrame]; |
| |
| // Null check. |
| [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:nil]; |
| SpringAnimation* keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation]; |
| XCTAssertTrue(keyboardSpringAnimation == nil); |
| |
| // CAAnimation that is not a CASpringAnimation. |
| CABasicAnimation* nonSpringAnimation = [CABasicAnimation animation]; |
| nonSpringAnimation.duration = 1.0; |
| nonSpringAnimation.fromValue = [NSNumber numberWithFloat:0.0]; |
| nonSpringAnimation.toValue = [NSNumber numberWithFloat:1.0]; |
| nonSpringAnimation.keyPath = @"position"; |
| [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:nonSpringAnimation]; |
| keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation]; |
| |
| XCTAssertTrue(keyboardSpringAnimation == nil); |
| |
| // CASpringAnimation. |
| CASpringAnimation* springAnimation = [CASpringAnimation animation]; |
| springAnimation.mass = 1.0; |
| springAnimation.stiffness = 100.0; |
| springAnimation.damping = 10.0; |
| springAnimation.keyPath = @"position"; |
| springAnimation.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)]; |
| springAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(100, 100)]; |
| [viewControllerMock setUpKeyboardSpringAnimationIfNeeded:springAnimation]; |
| keyboardSpringAnimation = [viewControllerMock keyboardSpringAnimation]; |
| XCTAssertTrue(keyboardSpringAnimation != nil); |
| } |
| |
| - (void)testKeyboardAnimationIsShowingAndCompounding { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| CGRect viewFrame = screen.bounds; |
| [self setUpMockView:viewControllerMock |
| screen:screen |
| viewFrame:viewFrame |
| convertedFrame:viewFrame]; |
| |
| BOOL isLocal = YES; |
| CGFloat screenHeight = screen.bounds.size.height; |
| CGFloat screenWidth = screen.bounds.size.height; |
| |
| // Start show keyboard animation. |
| CGRect initialShowKeyboardBeginFrame = CGRectMake(0, screenHeight, screenWidth, 250); |
| CGRect initialShowKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500); |
| NSNotification* fakeNotification = [NSNotification |
| notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameBeginUserInfoKey" : @(initialShowKeyboardBeginFrame), |
| @"UIKeyboardFrameEndUserInfoKey" : @(initialShowKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| viewControllerMock.targetViewInsetBottom = 0; |
| [viewControllerMock handleKeyboardNotification:fakeNotification]; |
| BOOL isShowingAnimation1 = viewControllerMock.keyboardAnimationIsShowing; |
| XCTAssertTrue(isShowingAnimation1); |
| |
| // Start compounding show keyboard animation. |
| CGRect compoundingShowKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250); |
| CGRect compoundingShowKeyboardEndFrame = CGRectMake(0, screenHeight - 500, screenWidth, 500); |
| fakeNotification = [NSNotification |
| notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingShowKeyboardBeginFrame), |
| @"UIKeyboardFrameEndUserInfoKey" : @(compoundingShowKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| |
| [viewControllerMock handleKeyboardNotification:fakeNotification]; |
| BOOL isShowingAnimation2 = viewControllerMock.keyboardAnimationIsShowing; |
| XCTAssertTrue(isShowingAnimation2); |
| XCTAssertTrue(isShowingAnimation1 == isShowingAnimation2); |
| |
| // Start hide keyboard animation. |
| CGRect initialHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 500, screenWidth, 250); |
| CGRect initialHideKeyboardEndFrame = CGRectMake(0, screenHeight - 250, screenWidth, 500); |
| fakeNotification = [NSNotification |
| notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameBeginUserInfoKey" : @(initialHideKeyboardBeginFrame), |
| @"UIKeyboardFrameEndUserInfoKey" : @(initialHideKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| |
| [viewControllerMock handleKeyboardNotification:fakeNotification]; |
| BOOL isShowingAnimation3 = viewControllerMock.keyboardAnimationIsShowing; |
| XCTAssertFalse(isShowingAnimation3); |
| XCTAssertTrue(isShowingAnimation2 != isShowingAnimation3); |
| |
| // Start compounding hide keyboard animation. |
| CGRect compoundingHideKeyboardBeginFrame = CGRectMake(0, screenHeight - 250, screenWidth, 250); |
| CGRect compoundingHideKeyboardEndFrame = CGRectMake(0, screenHeight, screenWidth, 500); |
| fakeNotification = [NSNotification |
| notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameBeginUserInfoKey" : @(compoundingHideKeyboardBeginFrame), |
| @"UIKeyboardFrameEndUserInfoKey" : @(compoundingHideKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| |
| [viewControllerMock handleKeyboardNotification:fakeNotification]; |
| BOOL isShowingAnimation4 = viewControllerMock.keyboardAnimationIsShowing; |
| XCTAssertFalse(isShowingAnimation4); |
| XCTAssertTrue(isShowingAnimation3 == isShowingAnimation4); |
| } |
| |
| - (void)testShouldIgnoreKeyboardNotification { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| CGRect viewFrame = screen.bounds; |
| [self setUpMockView:viewControllerMock |
| screen:screen |
| viewFrame:viewFrame |
| convertedFrame:viewFrame]; |
| |
| CGFloat screenWidth = screen.bounds.size.width; |
| CGFloat screenHeight = screen.bounds.size.height; |
| CGRect emptyKeyboard = CGRectZero; |
| CGRect zeroHeightKeyboard = CGRectMake(0, 0, screenWidth, 0); |
| CGRect validKeyboardEndFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320); |
| BOOL isLocal = NO; |
| |
| // Hide notification, valid keyboard |
| NSNotification* notification = |
| [NSNotification notificationWithName:UIKeyboardWillHideNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| |
| BOOL shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; |
| XCTAssertTrue(shouldIgnore == NO); |
| |
| // All zero keyboard |
| isLocal = YES; |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(emptyKeyboard), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; |
| XCTAssertTrue(shouldIgnore == YES); |
| |
| // Zero height keyboard |
| isLocal = NO; |
| notification = |
| [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(zeroHeightKeyboard), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; |
| XCTAssertTrue(shouldIgnore == NO); |
| |
| // Valid keyboard, triggered from another app |
| isLocal = NO; |
| notification = |
| [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; |
| XCTAssertTrue(shouldIgnore == YES); |
| |
| // Valid keyboard |
| isLocal = YES; |
| notification = |
| [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; |
| XCTAssertTrue(shouldIgnore == NO); |
| |
| if (@available(iOS 13.0, *)) { |
| // noop |
| } else { |
| // Valid keyboard, keyboard is in background |
| OCMStub([viewControllerMock isKeyboardInOrTransitioningFromBackground]).andReturn(YES); |
| |
| isLocal = YES; |
| notification = |
| [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(validKeyboardEndFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| shouldIgnore = [viewControllerMock shouldIgnoreKeyboardNotification:notification]; |
| XCTAssertTrue(shouldIgnore == YES); |
| } |
| } |
| - (void)testKeyboardAnimationWillNotCrashWhenEngineDestroyed { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| [viewController setUpKeyboardAnimationVsyncClient:^(fml::TimePoint){ |
| }]; |
| [engine destroyContext]; |
| } |
| |
| - (void)testKeyboardAnimationWillWaitUIThreadVsync { |
| // We need to make sure the new viewport metrics get sent after the |
| // begin frame event has processed. And this test is to expect that the callback |
| // will sync with UI thread. So just simulate a lot of works on UI thread and |
| // test the keyboard animation callback will execute until UI task completed. |
| // Related issue: https://github.com/flutter/flutter/issues/120555. |
| |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| // Post a task to UI thread to block the thread. |
| const int delayTime = 1; |
| [engine uiTaskRunner]->PostTask([] { sleep(delayTime); }); |
| XCTestExpectation* expectation = [self expectationWithDescription:@"keyboard animation callback"]; |
| |
| __block CFTimeInterval fulfillTime; |
| FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) { |
| fulfillTime = CACurrentMediaTime(); |
| [expectation fulfill]; |
| }; |
| CFTimeInterval startTime = CACurrentMediaTime(); |
| [viewController setUpKeyboardAnimationVsyncClient:callback]; |
| [self waitForExpectationsWithTimeout:5.0 handler:nil]; |
| XCTAssertTrue(fulfillTime - startTime > delayTime); |
| } |
| |
| - (void)testCalculateKeyboardAttachMode { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| CGRect viewFrame = screen.bounds; |
| [self setUpMockView:viewControllerMock |
| screen:screen |
| viewFrame:viewFrame |
| convertedFrame:viewFrame]; |
| |
| CGFloat screenWidth = screen.bounds.size.width; |
| CGFloat screenHeight = screen.bounds.size.height; |
| |
| // hide notification |
| CGRect keyboardFrame = CGRectZero; |
| NSNotification* notification = |
| [NSNotification notificationWithName:UIKeyboardWillHideNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| FlutterKeyboardMode keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden); |
| |
| // all zeros |
| keyboardFrame = CGRectZero; |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating); |
| |
| // 0 height |
| keyboardFrame = CGRectMake(0, 0, screenWidth, 0); |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden); |
| |
| // floating |
| keyboardFrame = CGRectMake(0, 0, 320, 320); |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating); |
| |
| // undocked |
| keyboardFrame = CGRectMake(0, 0, screenWidth, 320); |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeFloating); |
| |
| // docked |
| keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320); |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeDocked); |
| |
| // docked - rounded values |
| CGFloat longDecimalHeight = 320.666666666666666; |
| keyboardFrame = CGRectMake(0, screenHeight - longDecimalHeight, screenWidth, longDecimalHeight); |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeDocked); |
| |
| // hidden - rounded values |
| keyboardFrame = CGRectMake(0, screenHeight - .0000001, screenWidth, longDecimalHeight); |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden); |
| |
| // hidden |
| keyboardFrame = CGRectMake(0, screenHeight, screenWidth, 320); |
| notification = [NSNotification notificationWithName:UIKeyboardWillChangeFrameNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(YES) |
| }]; |
| keyboardMode = [viewControllerMock calculateKeyboardAttachMode:notification]; |
| XCTAssertTrue(keyboardMode == FlutterKeyboardModeHidden); |
| } |
| |
| - (void)testCalculateMultitaskingAdjustment { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| |
| UIScreen* screen = [self setUpMockScreen]; |
| CGFloat screenWidth = screen.bounds.size.width; |
| CGFloat screenHeight = screen.bounds.size.height; |
| CGRect screenRect = screen.bounds; |
| CGRect viewOrigFrame = CGRectMake(0, 0, 320, screenHeight - 40); |
| CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40); |
| CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300); |
| id mockView = [self setUpMockView:viewControllerMock |
| screen:screen |
| viewFrame:viewOrigFrame |
| convertedFrame:convertedViewFrame]; |
| id mockTraitCollection = OCMClassMock([UITraitCollection class]); |
| OCMStub([mockTraitCollection userInterfaceIdiom]).andReturn(UIUserInterfaceIdiomPad); |
| OCMStub([mockTraitCollection horizontalSizeClass]).andReturn(UIUserInterfaceSizeClassCompact); |
| OCMStub([mockTraitCollection verticalSizeClass]).andReturn(UIUserInterfaceSizeClassRegular); |
| OCMStub([mockView traitCollection]).andReturn(mockTraitCollection); |
| |
| CGFloat adjustment = [viewControllerMock calculateMultitaskingAdjustment:screenRect |
| keyboardFrame:keyboardFrame]; |
| XCTAssertTrue(adjustment == 20); |
| } |
| |
| - (void)testCalculateKeyboardInset { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen); |
| |
| CGFloat screenWidth = screen.bounds.size.width; |
| CGFloat screenHeight = screen.bounds.size.height; |
| CGRect viewOrigFrame = CGRectMake(0, 0, 320, screenHeight - 40); |
| CGRect convertedViewFrame = CGRectMake(20, 20, 320, screenHeight - 40); |
| CGRect keyboardFrame = CGRectMake(20, screenHeight - 320, screenWidth, 300); |
| |
| [self setUpMockView:viewControllerMock |
| screen:screen |
| viewFrame:viewOrigFrame |
| convertedFrame:convertedViewFrame]; |
| |
| CGFloat inset = [viewControllerMock calculateKeyboardInset:keyboardFrame |
| keyboardMode:FlutterKeyboardModeDocked]; |
| XCTAssertTrue(inset == 300 * screen.scale); |
| } |
| |
| - (void)testHandleKeyboardNotification { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| // keyboard is empty |
| UIScreen* screen = [self setUpMockScreen]; |
| CGFloat screenWidth = screen.bounds.size.width; |
| CGFloat screenHeight = screen.bounds.size.height; |
| CGRect keyboardFrame = CGRectMake(0, screenHeight - 320, screenWidth, 320); |
| CGRect viewFrame = screen.bounds; |
| BOOL isLocal = YES; |
| NSNotification* notification = |
| [NSNotification notificationWithName:UIKeyboardWillShowNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @0.25, |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| [self setUpMockView:viewControllerMock |
| screen:screen |
| viewFrame:viewFrame |
| convertedFrame:viewFrame]; |
| viewControllerMock.targetViewInsetBottom = 0; |
| XCTestExpectation* expectation = [self expectationWithDescription:@"update viewport"]; |
| OCMStub([viewControllerMock updateViewportMetricsIfNeeded]).andDo(^(NSInvocation* invocation) { |
| [expectation fulfill]; |
| }); |
| |
| [viewControllerMock handleKeyboardNotification:notification]; |
| XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 320 * screen.scale); |
| OCMVerify([viewControllerMock startKeyBoardAnimation:0.25]); |
| [self waitForExpectationsWithTimeout:5.0 handler:nil]; |
| } |
| |
| - (void)testEnsureBottomInsetIsZeroWhenKeyboardDismissed { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| CGRect keyboardFrame = CGRectZero; |
| BOOL isLocal = YES; |
| NSNotification* fakeNotification = |
| [NSNotification notificationWithName:UIKeyboardWillHideNotification |
| object:nil |
| userInfo:@{ |
| @"UIKeyboardFrameEndUserInfoKey" : @(keyboardFrame), |
| @"UIKeyboardAnimationDurationUserInfoKey" : @(0.25), |
| @"UIKeyboardIsLocalUserInfoKey" : @(isLocal) |
| }]; |
| |
| viewControllerMock.targetViewInsetBottom = 10; |
| [viewControllerMock handleKeyboardNotification:fakeNotification]; |
| XCTAssertTrue(viewControllerMock.targetViewInsetBottom == 0); |
| } |
| |
| - (void)testEnsureViewportMetricsWillInvokeAndDisplayLinkWillInvalidateInViewDidDisappear { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| id viewControllerMock = OCMPartialMock(viewController); |
| [viewControllerMock viewDidDisappear:YES]; |
| OCMVerify([viewControllerMock ensureViewportMetricsIsCorrect]); |
| OCMVerify([viewControllerMock invalidateKeyboardAnimationVSyncClient]); |
| } |
| |
| - (void)testViewDidDisappearDoesntPauseEngineWhenNotTheViewController { |
| id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; |
| mockEngine.lifecycleChannel = lifecycleChannel; |
| FlutterViewController* viewControllerA = |
| [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil]; |
| FlutterViewController* viewControllerB = |
| [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil]; |
| id viewControllerMock = OCMPartialMock(viewControllerA); |
| OCMStub([viewControllerMock surfaceUpdated:NO]); |
| mockEngine.viewController = viewControllerB; |
| [viewControllerA viewDidDisappear:NO]; |
| OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]); |
| OCMReject([viewControllerMock surfaceUpdated:[OCMArg any]]); |
| } |
| |
| - (void)testAppWillTerminateViewDidDestroyTheEngine { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| id viewControllerMock = OCMPartialMock(viewController); |
| OCMStub([viewControllerMock goToApplicationLifecycle:@"AppLifecycleState.detached"]); |
| OCMStub([mockEngine destroyContext]); |
| [viewController applicationWillTerminate:nil]; |
| OCMVerify([viewControllerMock goToApplicationLifecycle:@"AppLifecycleState.detached"]); |
| OCMVerify([mockEngine destroyContext]); |
| } |
| |
| - (void)testViewDidDisappearDoesPauseEngineWhenIsTheViewController { |
| id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; |
| mockEngine.lifecycleChannel = lifecycleChannel; |
| __weak FlutterViewController* weakViewController; |
| @autoreleasepool { |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| weakViewController = viewController; |
| id viewControllerMock = OCMPartialMock(viewController); |
| OCMStub([viewControllerMock surfaceUpdated:NO]); |
| [viewController viewDidDisappear:NO]; |
| OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.paused"]); |
| OCMVerify([viewControllerMock surfaceUpdated:NO]); |
| } |
| XCTAssertNil(weakViewController); |
| } |
| |
| - (void) |
| testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillAppear { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| [viewController viewWillAppear:YES]; |
| OCMVerify([viewController onUserSettingsChanged:nil]); |
| } |
| |
| - (void) |
| testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillAppear { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = nil; |
| FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = nil; |
| mockEngine.viewController = viewControllerB; |
| [viewControllerA viewWillAppear:YES]; |
| OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]); |
| } |
| |
| - (void) |
| testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewDidAppear { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| [viewController viewDidAppear:YES]; |
| OCMVerify([viewController onUserSettingsChanged:nil]); |
| } |
| |
| - (void) |
| testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewDidAppear { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = nil; |
| FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = nil; |
| mockEngine.viewController = viewControllerB; |
| [viewControllerA viewDidAppear:YES]; |
| OCMVerify(never(), [viewControllerA onUserSettingsChanged:nil]); |
| } |
| |
| - (void) |
| testEngineConfigSyncMethodWillExecuteWhenViewControllerInEngineIsCurrentViewControllerInViewWillDisappear { |
| id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; |
| mockEngine.lifecycleChannel = lifecycleChannel; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = viewController; |
| [viewController viewWillDisappear:NO]; |
| OCMVerify([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]); |
| } |
| |
| - (void) |
| testEngineConfigSyncMethodWillNotExecuteWhenViewControllerInEngineIsNotCurrentViewControllerInViewWillDisappear { |
| id lifecycleChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; |
| mockEngine.lifecycleChannel = lifecycleChannel; |
| FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = viewControllerB; |
| [viewControllerA viewDidDisappear:NO]; |
| OCMReject([lifecycleChannel sendMessage:@"AppLifecycleState.inactive"]); |
| } |
| |
| - (void)testUpdateViewportMetricsIfNeeded_DoesntInvokeEngineWhenNotTheViewController { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = nil; |
| FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = viewControllerB; |
| [viewControllerA updateViewportMetricsIfNeeded]; |
| flutter::ViewportMetrics viewportMetrics; |
| OCMVerify(never(), [mockEngine updateViewportMetrics:viewportMetrics]); |
| } |
| |
| - (void)testUpdateViewportMetricsIfNeeded_DoesInvokeEngineWhenIsTheViewController { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = viewController; |
| flutter::ViewportMetrics viewportMetrics; |
| OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs(); |
| [viewController updateViewportMetricsIfNeeded]; |
| OCMVerifyAll(mockEngine); |
| } |
| |
| - (void)testUpdateViewportMetricsIfNeeded_DoesNotInvokeEngineWhenShouldBeIgnoredDuringRotation { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen); |
| mockEngine.viewController = viewController; |
| |
| id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); |
| OCMStub([mockCoordinator transitionDuration]).andReturn(0.5); |
| |
| // Mimic the device rotation. |
| [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; |
| // Should not trigger the engine call when during rotation. |
| [viewController updateViewportMetricsIfNeeded]; |
| |
| OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]); |
| } |
| |
| - (void)testViewWillTransitionToSize_DoesDelayEngineCallIfNonZeroDuration { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen); |
| mockEngine.viewController = viewController; |
| |
| // Mimic the device rotation with non-zero transition duration. |
| NSTimeInterval transitionDuration = 0.5; |
| id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); |
| OCMStub([mockCoordinator transitionDuration]).andReturn(transitionDuration); |
| |
| flutter::ViewportMetrics viewportMetrics; |
| OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs(); |
| |
| [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; |
| // Should not immediately call the engine (this request should be ignored). |
| [viewController updateViewportMetricsIfNeeded]; |
| OCMVerify(never(), [mockEngine updateViewportMetrics:flutter::ViewportMetrics()]); |
| |
| // Should delay the engine call for half of the transition duration. |
| // Wait for additional transitionDuration to allow updateViewportMetrics calls if any. |
| XCTWaiterResult result = [XCTWaiter |
| waitForExpectations:@[ [self expectationWithDescription:@"Waiting for rotation duration"] ] |
| timeout:transitionDuration]; |
| XCTAssertEqual(result, XCTWaiterResultTimedOut); |
| |
| OCMVerifyAll(mockEngine); |
| } |
| |
| - (void)testViewWillTransitionToSize_DoesNotDelayEngineCallIfZeroDuration { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| FlutterViewController* viewControllerMock = OCMPartialMock(viewController); |
| UIScreen* screen = [self setUpMockScreen]; |
| OCMStub([viewControllerMock flutterScreenIfViewLoaded]).andReturn(screen); |
| mockEngine.viewController = viewController; |
| |
| // Mimic the device rotation with zero transition duration. |
| id mockCoordinator = OCMProtocolMock(@protocol(UIViewControllerTransitionCoordinator)); |
| OCMStub([mockCoordinator transitionDuration]).andReturn(0); |
| |
| flutter::ViewportMetrics viewportMetrics; |
| OCMExpect([mockEngine updateViewportMetrics:viewportMetrics]).ignoringNonObjectArgs(); |
| |
| // Should immediately trigger the engine call, without delay. |
| [viewController viewWillTransitionToSize:CGSizeZero withTransitionCoordinator:mockCoordinator]; |
| [viewController updateViewportMetricsIfNeeded]; |
| |
| OCMVerifyAll(mockEngine); |
| } |
| |
| - (void)testViewDidLoadDoesntInvokeEngineWhenNotTheViewController { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewControllerA = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = nil; |
| FlutterViewController* viewControllerB = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = viewControllerB; |
| UIView* view = viewControllerA.view; |
| XCTAssertNotNil(view); |
| OCMVerify(never(), [mockEngine attachView]); |
| } |
| |
| - (void)testViewDidLoadDoesInvokeEngineWhenIsTheViewController { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| mockEngine.viewController = nil; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| mockEngine.viewController = viewController; |
| UIView* view = viewController.view; |
| XCTAssertNotNil(view); |
| OCMVerify(times(1), [mockEngine attachView]); |
| } |
| |
| - (void)testViewDidLoadDoesntInvokeEngineAttachViewWhenEngineNeedsLaunch { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| mockEngine.viewController = nil; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| // sharedSetupWithProject sets the engine needs to be launched. |
| [viewController sharedSetupWithProject:nil initialRoute:nil]; |
| mockEngine.viewController = viewController; |
| UIView* view = viewController.view; |
| XCTAssertNotNil(view); |
| OCMVerify(never(), [mockEngine attachView]); |
| } |
| |
| - (void)testSplashScreenViewRemoveNotCrash { |
| FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"engine" project:nil]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; |
| [flutterViewController setSplashScreenView:[[UIView alloc] init]]; |
| [flutterViewController setSplashScreenView:nil]; |
| } |
| |
| - (void)testInternalPluginsWeakPtrNotCrash { |
| FlutterSendKeyEvent sendEvent; |
| @autoreleasepool { |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithProject:nil |
| nibName:nil |
| bundle:nil]; |
| [vc addInternalPlugins]; |
| FlutterKeyboardManager* keyboardManager = vc.keyboardManager; |
| FlutterEmbedderKeyResponder* keyPrimaryResponder = (FlutterEmbedderKeyResponder*) |
| [(NSArray<id<FlutterKeyPrimaryResponder>>*)keyboardManager.primaryResponders firstObject]; |
| sendEvent = [keyPrimaryResponder sendEvent]; |
| } |
| |
| if (sendEvent) { |
| sendEvent({}, nil, nil); |
| } |
| } |
| |
| // Regression test for https://github.com/flutter/engine/pull/32098. |
| - (void)testInternalPluginsInvokeInViewDidLoad { |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| UIView* view = viewController.view; |
| // The implementation in viewDidLoad requires the viewControllers.viewLoaded is true. |
| // Accessing the view to make sure the view loads in the memory, |
| // which makes viewControllers.viewLoaded true. |
| XCTAssertNotNil(view); |
| [viewController viewDidLoad]; |
| OCMVerify([viewController addInternalPlugins]); |
| } |
| |
| - (void)testBinaryMessenger { |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertNotNil(vc); |
| id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); |
| OCMStub([self.mockEngine binaryMessenger]).andReturn(messenger); |
| XCTAssertEqual(vc.binaryMessenger, messenger); |
| OCMVerify([self.mockEngine binaryMessenger]); |
| } |
| |
| - (void)testViewControllerIsReleased { |
| __weak FlutterViewController* weakViewController; |
| @autoreleasepool { |
| FlutterViewController* viewController = [[FlutterViewController alloc] init]; |
| weakViewController = viewController; |
| [viewController viewDidLoad]; |
| } |
| XCTAssertNil(weakViewController); |
| } |
| |
| #pragma mark - Platform Brightness |
| |
| - (void)testItReportsLightPlatformBrightnessByDefault { |
| // Setup test. |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| // Exercise behavior under test. |
| [vc traitCollectionDidChange:nil]; |
| |
| // Verify behavior. |
| OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { |
| return [message[@"platformBrightness"] isEqualToString:@"light"]; |
| }]]); |
| |
| // Clean up mocks |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsPlatformBrightnessWhenViewWillAppear { |
| // Setup test. |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel); |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| // Exercise behavior under test. |
| [vc viewWillAppear:false]; |
| |
| // Verify behavior. |
| OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { |
| return [message[@"platformBrightness"] isEqualToString:@"light"]; |
| }]]); |
| |
| // Clean up mocks |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel); |
| id mockTraitCollection = |
| [self fakeTraitCollectionWithUserInterfaceStyle:UIUserInterfaceStyleDark]; |
| |
| // We partially mock the real FlutterViewController to act as the OS and report |
| // the UITraitCollection of our choice. Mocking the object under test is not |
| // desirable, but given that the OS does not offer a DI approach to providing |
| // our own UITraitCollection, this seems to be the least bad option. |
| id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]); |
| OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection); |
| |
| // Exercise behavior under test. |
| [partialMockVC traitCollectionDidChange:nil]; |
| |
| // Verify behavior. |
| OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { |
| return [message[@"platformBrightness"] isEqualToString:@"dark"]; |
| }]]); |
| |
| // Clean up mocks |
| [partialMockVC stopMocking]; |
| [settingsChannel stopMocking]; |
| [mockTraitCollection stopMocking]; |
| } |
| |
| // Creates a mocked UITraitCollection with nil values for everything except userInterfaceStyle, |
| // which is set to the given "style". |
| - (UITraitCollection*)fakeTraitCollectionWithUserInterfaceStyle:(UIUserInterfaceStyle)style { |
| id mockTraitCollection = OCMClassMock([UITraitCollection class]); |
| OCMStub([mockTraitCollection userInterfaceStyle]).andReturn(style); |
| return mockTraitCollection; |
| } |
| |
| #pragma mark - Platform Contrast |
| |
| - (void)testItReportsNormalPlatformContrastByDefault { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| // Exercise behavior under test. |
| [vc traitCollectionDidChange:nil]; |
| |
| // Verify behavior. |
| OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { |
| return [message[@"platformContrast"] isEqualToString:@"normal"]; |
| }]]); |
| |
| // Clean up mocks |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsPlatformContrastWhenViewWillAppear { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| FlutterEngine* mockEngine = OCMPartialMock([[FlutterEngine alloc] init]); |
| [mockEngine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| |
| // Setup test. |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([mockEngine settingsChannel]).andReturn(settingsChannel); |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| // Exercise behavior under test. |
| [vc viewWillAppear:false]; |
| |
| // Verify behavior. |
| OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { |
| return [message[@"platformContrast"] isEqualToString:@"normal"]; |
| }]]); |
| |
| // Clean up mocks |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsHighContrastWhenTraitCollectionRequestsIt { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([self.mockEngine settingsChannel]).andReturn(settingsChannel); |
| |
| id mockTraitCollection = [self fakeTraitCollectionWithContrast:UIAccessibilityContrastHigh]; |
| |
| // We partially mock the real FlutterViewController to act as the OS and report |
| // the UITraitCollection of our choice. Mocking the object under test is not |
| // desirable, but given that the OS does not offer a DI approach to providing |
| // our own UITraitCollection, this seems to be the least bad option. |
| id partialMockVC = OCMPartialMock([[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]); |
| OCMStub([partialMockVC traitCollection]).andReturn(mockTraitCollection); |
| |
| // Exercise behavior under test. |
| [partialMockVC traitCollectionDidChange:mockTraitCollection]; |
| |
| // Verify behavior. |
| OCMVerify([settingsChannel sendMessage:[OCMArg checkWithBlock:^BOOL(id message) { |
| return [message[@"platformContrast"] isEqualToString:@"high"]; |
| }]]); |
| |
| // Clean up mocks |
| [partialMockVC stopMocking]; |
| [settingsChannel stopMocking]; |
| [mockTraitCollection stopMocking]; |
| } |
| |
| - (void)testItReportsAccessibilityOnOffSwitchLabelsFlagNotSet { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| FlutterViewController* viewController = |
| [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil]; |
| id partialMockViewController = OCMPartialMock(viewController); |
| OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(NO); |
| |
| // Exercise behavior under test. |
| int32_t flags = [partialMockViewController accessibilityFlags]; |
| |
| // Verify behavior. |
| XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) == 0); |
| } |
| |
| - (void)testItReportsAccessibilityOnOffSwitchLabelsFlagSet { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| FlutterViewController* viewController = |
| [[FlutterViewController alloc] initWithEngine:self.mockEngine nibName:nil bundle:nil]; |
| id partialMockViewController = OCMPartialMock(viewController); |
| OCMStub([partialMockViewController accessibilityIsOnOffSwitchLabelsEnabled]).andReturn(YES); |
| |
| // Exercise behavior under test. |
| int32_t flags = [partialMockViewController accessibilityFlags]; |
| |
| // Verify behavior. |
| XCTAssert((flags & (int32_t)flutter::AccessibilityFeatureFlag::kOnOffSwitchLabels) != 0); |
| } |
| |
| - (void)testPerformOrientationUpdateForcesOrientationChange { |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait |
| currentOrientation:UIInterfaceOrientationLandscapeLeft |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationPortrait]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait |
| currentOrientation:UIInterfaceOrientationLandscapeRight |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationPortrait]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait |
| currentOrientation:UIInterfaceOrientationPortraitUpsideDown |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationPortrait]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown |
| currentOrientation:UIInterfaceOrientationLandscapeLeft |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationPortraitUpsideDown]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown |
| currentOrientation:UIInterfaceOrientationLandscapeRight |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationPortraitUpsideDown]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown |
| currentOrientation:UIInterfaceOrientationPortrait |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationPortraitUpsideDown]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape |
| currentOrientation:UIInterfaceOrientationPortrait |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeLeft]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape |
| currentOrientation:UIInterfaceOrientationPortraitUpsideDown |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeLeft]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft |
| currentOrientation:UIInterfaceOrientationPortrait |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeLeft]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft |
| currentOrientation:UIInterfaceOrientationLandscapeRight |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeLeft]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft |
| currentOrientation:UIInterfaceOrientationPortraitUpsideDown |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeLeft]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight |
| currentOrientation:UIInterfaceOrientationPortrait |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeRight]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight |
| currentOrientation:UIInterfaceOrientationLandscapeLeft |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeRight]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight |
| currentOrientation:UIInterfaceOrientationPortraitUpsideDown |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationLandscapeRight]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown |
| currentOrientation:UIInterfaceOrientationPortraitUpsideDown |
| didChangeOrientation:YES |
| resultingOrientation:UIInterfaceOrientationPortrait]; |
| } |
| |
| - (void)testPerformOrientationUpdateDoesNotForceOrientationChange { |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll |
| currentOrientation:UIInterfaceOrientationPortrait |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll |
| currentOrientation:UIInterfaceOrientationPortraitUpsideDown |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll |
| currentOrientation:UIInterfaceOrientationLandscapeLeft |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll |
| currentOrientation:UIInterfaceOrientationLandscapeRight |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown |
| currentOrientation:UIInterfaceOrientationPortrait |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown |
| currentOrientation:UIInterfaceOrientationLandscapeLeft |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown |
| currentOrientation:UIInterfaceOrientationLandscapeRight |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait |
| currentOrientation:UIInterfaceOrientationPortrait |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown |
| currentOrientation:UIInterfaceOrientationPortraitUpsideDown |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape |
| currentOrientation:UIInterfaceOrientationLandscapeLeft |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape |
| currentOrientation:UIInterfaceOrientationLandscapeRight |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft |
| currentOrientation:UIInterfaceOrientationLandscapeLeft |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| |
| [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight |
| currentOrientation:UIInterfaceOrientationLandscapeRight |
| didChangeOrientation:NO |
| resultingOrientation:static_cast<UIInterfaceOrientation>(0)]; |
| } |
| |
| // Perform an orientation update test that fails when the expected outcome |
| // for an orientation update is not met |
| - (void)orientationTestWithOrientationUpdate:(UIInterfaceOrientationMask)mask |
| currentOrientation:(UIInterfaceOrientation)currentOrientation |
| didChangeOrientation:(BOOL)didChange |
| resultingOrientation:(UIInterfaceOrientation)resultingOrientation { |
| id mockApplication = OCMClassMock([UIApplication class]); |
| id mockWindowScene; |
| id deviceMock; |
| id mockVC; |
| __block __weak id weakPreferences; |
| @autoreleasepool { |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| if (@available(iOS 16.0, *)) { |
| mockWindowScene = OCMClassMock([UIWindowScene class]); |
| mockVC = OCMPartialMock(realVC); |
| OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene); |
| if (realVC.supportedInterfaceOrientations == mask) { |
| OCMReject([mockWindowScene requestGeometryUpdateWithPreferences:[OCMArg any] |
| errorHandler:[OCMArg any]]); |
| } else { |
| // iOS 16 will decide whether to rotate based on the new preference, so always set it |
| // when it changes. |
| OCMExpect([mockWindowScene |
| requestGeometryUpdateWithPreferences:[OCMArg checkWithBlock:^BOOL( |
| UIWindowSceneGeometryPreferencesIOS* |
| preferences) { |
| weakPreferences = preferences; |
| return preferences.interfaceOrientations == mask; |
| }] |
| errorHandler:[OCMArg any]]); |
| } |
| OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); |
| OCMStub([mockApplication connectedScenes]).andReturn([NSSet setWithObject:mockWindowScene]); |
| } else { |
| deviceMock = OCMPartialMock([UIDevice currentDevice]); |
| if (!didChange) { |
| OCMReject([deviceMock setValue:[OCMArg any] forKey:@"orientation"]); |
| } else { |
| OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]); |
| } |
| if (@available(iOS 13.0, *)) { |
| mockWindowScene = OCMClassMock([UIWindowScene class]); |
| mockVC = OCMPartialMock(realVC); |
| OCMStub([mockVC flutterWindowSceneIfViewLoaded]).andReturn(mockWindowScene); |
| OCMStub(((UIWindowScene*)mockWindowScene).interfaceOrientation) |
| .andReturn(currentOrientation); |
| } else { |
| OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); |
| OCMStub([mockApplication statusBarOrientation]).andReturn(currentOrientation); |
| } |
| } |
| |
| [realVC performOrientationUpdate:mask]; |
| if (@available(iOS 16.0, *)) { |
| OCMVerifyAll(mockWindowScene); |
| } else { |
| OCMVerifyAll(deviceMock); |
| } |
| } |
| [mockWindowScene stopMocking]; |
| [deviceMock stopMocking]; |
| [mockApplication stopMocking]; |
| XCTAssertNil(weakPreferences); |
| } |
| |
| // Creates a mocked UITraitCollection with nil values for everything except accessibilityContrast, |
| // which is set to the given "contrast". |
| - (UITraitCollection*)fakeTraitCollectionWithContrast:(UIAccessibilityContrast)contrast { |
| id mockTraitCollection = OCMClassMock([UITraitCollection class]); |
| OCMStub([mockTraitCollection accessibilityContrast]).andReturn(contrast); |
| return mockTraitCollection; |
| } |
| |
| - (void)testWillDeallocNotification { |
| XCTestExpectation* expectation = |
| [[XCTestExpectation alloc] initWithDescription:@"notification called"]; |
| id engine = [[MockEngine alloc] init]; |
| @autoreleasepool { |
| // NOLINTNEXTLINE(clang-analyzer-deadcode.DeadStores) |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc |
| object:nil |
| queue:[NSOperationQueue mainQueue] |
| usingBlock:^(NSNotification* _Nonnull note) { |
| [expectation fulfill]; |
| }]; |
| XCTAssertNotNil(realVC); |
| realVC = nil; |
| } |
| [self waitForExpectations:@[ expectation ] timeout:1.0]; |
| } |
| |
| - (void)testReleasesKeyboardManagerOnDealloc { |
| __weak FlutterKeyboardManager* weakKeyboardManager = nil; |
| @autoreleasepool { |
| FlutterViewController* viewController = [[FlutterViewController alloc] init]; |
| |
| [viewController addInternalPlugins]; |
| weakKeyboardManager = viewController.keyboardManager; |
| XCTAssertNotNil(weakKeyboardManager); |
| [viewController deregisterNotifications]; |
| viewController = nil; |
| } |
| // View controller has released the keyboard manager. |
| XCTAssertNil(weakKeyboardManager); |
| } |
| |
| - (void)testDoesntLoadViewInInit { |
| FlutterDartProject* project = [[FlutterDartProject alloc] init]; |
| FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; |
| [engine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown"); |
| engine.viewController = nil; |
| } |
| |
| - (void)testHideOverlay { |
| FlutterDartProject* project = [[FlutterDartProject alloc] init]; |
| FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; |
| [engine createShell:@"" libraryURI:@"" initialRoute:nil]; |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @""); |
| [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator |
| object:nil]; |
| XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @""); |
| engine.viewController = nil; |
| } |
| |
| - (void)testNotifyLowMemory { |
| FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| id viewControllerMock = OCMPartialMock(viewController); |
| OCMStub([viewControllerMock surfaceUpdated:NO]); |
| [viewController beginAppearanceTransition:NO animated:NO]; |
| [viewController endAppearanceTransition]; |
| XCTAssertTrue(mockEngine.didCallNotifyLowMemory); |
| } |
| |
| - (void)sendMessage:(id _Nullable)message reply:(FlutterReply _Nullable)callback { |
| NSMutableDictionary* replyMessage = [@{ |
| @"handled" : @YES, |
| } mutableCopy]; |
| // Response is async, so we have to post it to the run loop instead of calling |
| // it directly. |
| self.messageSent = message; |
| CFRunLoopPerformBlock(CFRunLoopGetCurrent(), fml::MessageLoopDarwin::kMessageLoopCFRunLoopMode, |
| ^() { |
| callback(replyMessage); |
| }); |
| } |
| |
| - (void)testValidKeyUpEvent API_AVAILABLE(ios(13.4)) { |
| if (@available(iOS 13.4, *)) { |
| // noop |
| } else { |
| return; |
| } |
| FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; |
| mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]]) |
| .andCall(self, @selector(sendMessage:reply:)); |
| OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES); |
| mockEngine.textInputPlugin = self.mockTextInputPlugin; |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| // Allocate the keyboard manager in the view controller by adding the internal |
| // plugins. |
| [vc addInternalPlugins]; |
| |
| [vc handlePressEvent:keyUpEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0) |
| nextAction:^(){ |
| }]; |
| |
| XCTAssert(self.messageSent != nil); |
| XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]); |
| XCTAssert([self.messageSent[@"type"] isEqualToString:@"keyup"]); |
| XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]); |
| XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]); |
| XCTAssert([self.messageSent[@"characters"] isEqualToString:@""]); |
| XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@""]); |
| [vc deregisterNotifications]; |
| } |
| |
| - (void)testValidKeyDownEvent API_AVAILABLE(ios(13.4)) { |
| if (@available(iOS 13.4, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| FlutterEnginePartialMock* mockEngine = [[FlutterEnginePartialMock alloc] init]; |
| mockEngine.keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([mockEngine.keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]]) |
| .andCall(self, @selector(sendMessage:reply:)); |
| OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES); |
| mockEngine.textInputPlugin = self.mockTextInputPlugin; |
| |
| __strong FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:mockEngine |
| nibName:nil |
| bundle:nil]; |
| // Allocate the keyboard manager in the view controller by adding the internal |
| // plugins. |
| [vc addInternalPlugins]; |
| |
| [vc handlePressEvent:keyDownEvent(UIKeyboardHIDUsageKeyboardA, UIKeyModifierShift, 123.0f, "A", |
| "a") |
| nextAction:^(){ |
| }]; |
| |
| XCTAssert(self.messageSent != nil); |
| XCTAssert([self.messageSent[@"keymap"] isEqualToString:@"ios"]); |
| XCTAssert([self.messageSent[@"type"] isEqualToString:@"keydown"]); |
| XCTAssert([self.messageSent[@"keyCode"] isEqualToNumber:[NSNumber numberWithInt:4]]); |
| XCTAssert([self.messageSent[@"modifiers"] isEqualToNumber:[NSNumber numberWithInt:0]]); |
| XCTAssert([self.messageSent[@"characters"] isEqualToString:@"A"]); |
| XCTAssert([self.messageSent[@"charactersIgnoringModifiers"] isEqualToString:@"a"]); |
| [vc deregisterNotifications]; |
| vc = nil; |
| } |
| |
| - (void)testIgnoredKeyEvents API_AVAILABLE(ios(13.4)) { |
| if (@available(iOS 13.4, *)) { |
| // noop |
| } else { |
| return; |
| } |
| id keyEventChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([keyEventChannel sendMessage:[OCMArg any] reply:[OCMArg any]]) |
| .andCall(self, @selector(sendMessage:reply:)); |
| OCMStub([self.mockTextInputPlugin handlePress:[OCMArg any]]).andReturn(YES); |
| OCMStub([self.mockEngine keyEventChannel]).andReturn(keyEventChannel); |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| |
| // Allocate the keyboard manager in the view controller by adding the internal |
| // plugins. |
| [vc addInternalPlugins]; |
| |
| [vc handlePressEvent:keyEventWithPhase(UIPressPhaseStationary, UIKeyboardHIDUsageKeyboardA, |
| UIKeyModifierShift, 123.0) |
| nextAction:^(){ |
| }]; |
| [vc handlePressEvent:keyEventWithPhase(UIPressPhaseCancelled, UIKeyboardHIDUsageKeyboardA, |
| UIKeyModifierShift, 123.0) |
| nextAction:^(){ |
| }]; |
| [vc handlePressEvent:keyEventWithPhase(UIPressPhaseChanged, UIKeyboardHIDUsageKeyboardA, |
| UIKeyModifierShift, 123.0) |
| nextAction:^(){ |
| }]; |
| |
| XCTAssert(self.messageSent == nil); |
| OCMVerify(never(), [keyEventChannel sendMessage:[OCMArg any]]); |
| [vc deregisterNotifications]; |
| } |
| |
| - (void)testPanGestureRecognizer API_AVAILABLE(ios(13.4)) { |
| if (@available(iOS 13.4, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertNotNil(vc); |
| UIView* view = vc.view; |
| XCTAssertNotNil(view); |
| NSArray* gestureRecognizers = view.gestureRecognizers; |
| XCTAssertNotNil(gestureRecognizers); |
| |
| BOOL found = NO; |
| for (id gesture in gestureRecognizers) { |
| if ([gesture isKindOfClass:[UIPanGestureRecognizer class]]) { |
| found = YES; |
| break; |
| } |
| } |
| XCTAssertTrue(found); |
| } |
| |
| - (void)testMouseSupport API_AVAILABLE(ios(13.4)) { |
| if (@available(iOS 13.4, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertNotNil(vc); |
| |
| id mockPanGestureRecognizer = OCMClassMock([UIPanGestureRecognizer class]); |
| XCTAssertNotNil(mockPanGestureRecognizer); |
| |
| [vc discreteScrollEvent:mockPanGestureRecognizer]; |
| |
| [[[self.mockEngine verify] ignoringNonObjectArgs] |
| dispatchPointerDataPacket:std::make_unique<flutter::PointerDataPacket>(0)]; |
| } |
| |
| - (void)testFakeEventTimeStamp { |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:self.mockEngine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertNotNil(vc); |
| |
| flutter::PointerData pointer_data = [vc generatePointerDataForFake]; |
| int64_t current_micros = [[NSProcessInfo processInfo] systemUptime] * 1000 * 1000; |
| int64_t interval_micros = current_micros - pointer_data.time_stamp; |
| const int64_t tolerance_millis = 2; |
| XCTAssertTrue(interval_micros / 1000 < tolerance_millis, |
| @"PointerData.time_stamp should be equal to NSProcessInfo.systemUptime"); |
| } |
| |
| - (void)testSplashScreenViewCanSetNil { |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithProject:nil nibName:nil bundle:nil]; |
| [flutterViewController setSplashScreenView:nil]; |
| } |
| |
| - (void)testLifeCycleNotificationBecameActive { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; |
| UIWindow* window = [[UIWindow alloc] init]; |
| [window addSubview:flutterViewController.view]; |
| flutterViewController.view.bounds = CGRectMake(0, 0, 100, 100); |
| [flutterViewController viewDidLayoutSubviews]; |
| NSNotification* sceneNotification = |
| [NSNotification notificationWithName:UISceneDidActivateNotification object:nil userInfo:nil]; |
| NSNotification* applicationNotification = |
| [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification |
| object:nil |
| userInfo:nil]; |
| id mockVC = OCMPartialMock(flutterViewController); |
| [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; |
| [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
| #if APPLICATION_EXTENSION_API_ONLY |
| OCMVerify([mockVC sceneBecameActive:[OCMArg any]]); |
| OCMReject([mockVC applicationBecameActive:[OCMArg any]]); |
| #else |
| OCMReject([mockVC sceneBecameActive:[OCMArg any]]); |
| OCMVerify([mockVC applicationBecameActive:[OCMArg any]]); |
| #endif |
| XCTAssertFalse(flutterViewController.isKeyboardInOrTransitioningFromBackground); |
| OCMVerify([mockVC surfaceUpdated:YES]); |
| XCTestExpectation* timeoutApplicationLifeCycle = |
| [self expectationWithDescription:@"timeoutApplicationLifeCycle"]; |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), |
| dispatch_get_main_queue(), ^{ |
| [timeoutApplicationLifeCycle fulfill]; |
| OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]); |
| [flutterViewController deregisterNotifications]; |
| }); |
| [self waitForExpectationsWithTimeout:5.0 handler:nil]; |
| } |
| |
| - (void)testLifeCycleNotificationWillResignActive { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; |
| NSNotification* sceneNotification = |
| [NSNotification notificationWithName:UISceneWillDeactivateNotification |
| object:nil |
| userInfo:nil]; |
| NSNotification* applicationNotification = |
| [NSNotification notificationWithName:UIApplicationWillResignActiveNotification |
| object:nil |
| userInfo:nil]; |
| id mockVC = OCMPartialMock(flutterViewController); |
| [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; |
| [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
| #if APPLICATION_EXTENSION_API_ONLY |
| OCMVerify([mockVC sceneWillResignActive:[OCMArg any]]); |
| OCMReject([mockVC applicationWillResignActive:[OCMArg any]]); |
| #else |
| OCMReject([mockVC sceneWillResignActive:[OCMArg any]]); |
| OCMVerify([mockVC applicationWillResignActive:[OCMArg any]]); |
| #endif |
| OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); |
| [flutterViewController deregisterNotifications]; |
| } |
| |
| - (void)testLifeCycleNotificationWillTerminate { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; |
| NSNotification* sceneNotification = |
| [NSNotification notificationWithName:UISceneDidDisconnectNotification |
| object:nil |
| userInfo:nil]; |
| NSNotification* applicationNotification = |
| [NSNotification notificationWithName:UIApplicationWillTerminateNotification |
| object:nil |
| userInfo:nil]; |
| id mockVC = OCMPartialMock(flutterViewController); |
| id mockEngine = OCMPartialMock(engine); |
| OCMStub([mockVC engine]).andReturn(mockEngine); |
| [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; |
| [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
| #if APPLICATION_EXTENSION_API_ONLY |
| OCMVerify([mockVC sceneWillDisconnect:[OCMArg any]]); |
| OCMReject([mockVC applicationWillTerminate:[OCMArg any]]); |
| #else |
| OCMReject([mockVC sceneWillDisconnect:[OCMArg any]]); |
| OCMVerify([mockVC applicationWillTerminate:[OCMArg any]]); |
| #endif |
| OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.detached"]); |
| OCMVerify([mockEngine destroyContext]); |
| [flutterViewController deregisterNotifications]; |
| } |
| |
| - (void)testLifeCycleNotificationDidEnterBackground { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; |
| NSNotification* sceneNotification = |
| [NSNotification notificationWithName:UISceneDidEnterBackgroundNotification |
| object:nil |
| userInfo:nil]; |
| NSNotification* applicationNotification = |
| [NSNotification notificationWithName:UIApplicationDidEnterBackgroundNotification |
| object:nil |
| userInfo:nil]; |
| id mockVC = OCMPartialMock(flutterViewController); |
| [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; |
| [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
| #if APPLICATION_EXTENSION_API_ONLY |
| OCMVerify([mockVC sceneDidEnterBackground:[OCMArg any]]); |
| OCMReject([mockVC applicationDidEnterBackground:[OCMArg any]]); |
| #else |
| OCMReject([mockVC sceneDidEnterBackground:[OCMArg any]]); |
| OCMVerify([mockVC applicationDidEnterBackground:[OCMArg any]]); |
| #endif |
| XCTAssertTrue(flutterViewController.isKeyboardInOrTransitioningFromBackground); |
| OCMVerify([mockVC surfaceUpdated:NO]); |
| OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.paused"]); |
| [flutterViewController deregisterNotifications]; |
| } |
| |
| - (void)testLifeCycleNotificationWillEnterForeground { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; |
| NSNotification* sceneNotification = |
| [NSNotification notificationWithName:UISceneWillEnterForegroundNotification |
| object:nil |
| userInfo:nil]; |
| NSNotification* applicationNotification = |
| [NSNotification notificationWithName:UIApplicationWillEnterForegroundNotification |
| object:nil |
| userInfo:nil]; |
| id mockVC = OCMPartialMock(flutterViewController); |
| [[NSNotificationCenter defaultCenter] postNotification:sceneNotification]; |
| [[NSNotificationCenter defaultCenter] postNotification:applicationNotification]; |
| #if APPLICATION_EXTENSION_API_ONLY |
| OCMVerify([mockVC sceneWillEnterForeground:[OCMArg any]]); |
| OCMReject([mockVC applicationWillEnterForeground:[OCMArg any]]); |
| #else |
| OCMReject([mockVC sceneWillEnterForeground:[OCMArg any]]); |
| OCMVerify([mockVC applicationWillEnterForeground:[OCMArg any]]); |
| #endif |
| OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); |
| [flutterViewController deregisterNotifications]; |
| } |
| |
| - (void)testLifeCycleNotificationCancelledInvalidResumed { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* flutterViewController = |
| [[FlutterViewController alloc] initWithEngine:engine nibName:nil bundle:nil]; |
| NSNotification* applicationDidBecomeActiveNotification = |
| [NSNotification notificationWithName:UIApplicationDidBecomeActiveNotification |
| object:nil |
| userInfo:nil]; |
| NSNotification* applicationWillResignActiveNotification = |
| [NSNotification notificationWithName:UIApplicationWillResignActiveNotification |
| object:nil |
| userInfo:nil]; |
| id mockVC = OCMPartialMock(flutterViewController); |
| [[NSNotificationCenter defaultCenter] postNotification:applicationDidBecomeActiveNotification]; |
| [[NSNotificationCenter defaultCenter] postNotification:applicationWillResignActiveNotification]; |
| #if APPLICATION_EXTENSION_API_ONLY |
| #else |
| OCMVerify([mockVC goToApplicationLifecycle:@"AppLifecycleState.inactive"]); |
| #endif |
| |
| XCTestExpectation* timeoutApplicationLifeCycle = |
| [self expectationWithDescription:@"timeoutApplicationLifeCycle"]; |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), |
| dispatch_get_main_queue(), ^{ |
| OCMReject([mockVC goToApplicationLifecycle:@"AppLifecycleState.resumed"]); |
| [timeoutApplicationLifeCycle fulfill]; |
| [flutterViewController deregisterNotifications]; |
| }); |
| [self waitForExpectationsWithTimeout:5.0 handler:nil]; |
| } |
| |
| - (void)testSetupKeyboardAnimationVsyncClientWillCreateNewVsyncClientForFlutterViewController { |
| id bundleMock = OCMPartialMock([NSBundle mainBundle]); |
| OCMStub([bundleMock objectForInfoDictionaryKey:@"CADisableMinimumFrameDurationOnPhone"]) |
| .andReturn(@YES); |
| id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; |
| double maxFrameRate = 120; |
| [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| FlutterKeyboardAnimationCallback callback = ^(fml::TimePoint targetTime) { |
| }; |
| [viewController setUpKeyboardAnimationVsyncClient:callback]; |
| XCTAssertNotNil(viewController.keyboardAnimationVSyncClient); |
| CADisplayLink* link = [viewController.keyboardAnimationVSyncClient getDisplayLink]; |
| XCTAssertNotNil(link); |
| if (@available(iOS 15.0, *)) { |
| XCTAssertEqual(link.preferredFrameRateRange.maximum, maxFrameRate); |
| XCTAssertEqual(link.preferredFrameRateRange.preferred, maxFrameRate); |
| XCTAssertEqual(link.preferredFrameRateRange.minimum, maxFrameRate / 2); |
| } else { |
| XCTAssertEqual(link.preferredFramesPerSecond, maxFrameRate); |
| } |
| } |
| |
| - (void) |
| testCreateTouchRateCorrectionVSyncClientWillCreateVsyncClientWhenRefreshRateIsLargerThan60HZ { |
| id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; |
| double maxFrameRate = 120; |
| [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| [viewController createTouchRateCorrectionVSyncClientIfNeeded]; |
| XCTAssertNotNil(viewController.touchRateCorrectionVSyncClient); |
| } |
| |
| - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateNewVSyncClientWhenClientAlreadyExists { |
| id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; |
| double maxFrameRate = 120; |
| [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; |
| |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| [viewController createTouchRateCorrectionVSyncClientIfNeeded]; |
| VSyncClient* clientBefore = viewController.touchRateCorrectionVSyncClient; |
| XCTAssertNotNil(clientBefore); |
| |
| [viewController createTouchRateCorrectionVSyncClientIfNeeded]; |
| VSyncClient* clientAfter = viewController.touchRateCorrectionVSyncClient; |
| XCTAssertNotNil(clientAfter); |
| |
| XCTAssertTrue(clientBefore == clientAfter); |
| } |
| |
| - (void)testCreateTouchRateCorrectionVSyncClientWillNotCreateVsyncClientWhenRefreshRateIs60HZ { |
| id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; |
| double maxFrameRate = 60; |
| [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| [viewController createTouchRateCorrectionVSyncClientIfNeeded]; |
| XCTAssertNil(viewController.touchRateCorrectionVSyncClient); |
| } |
| |
| - (void)testTriggerTouchRateCorrectionVSyncClientCorrectly { |
| id mockDisplayLinkManager = [OCMockObject mockForClass:[DisplayLinkManager class]]; |
| double maxFrameRate = 120; |
| [[[mockDisplayLinkManager stub] andReturnValue:@(maxFrameRate)] displayRefreshRate]; |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| [viewController loadView]; |
| [viewController viewDidLoad]; |
| |
| VSyncClient* client = viewController.touchRateCorrectionVSyncClient; |
| CADisplayLink* link = [client getDisplayLink]; |
| |
| UITouch* fakeTouchBegan = [[UITouch alloc] init]; |
| fakeTouchBegan.phase = UITouchPhaseBegan; |
| |
| UITouch* fakeTouchMove = [[UITouch alloc] init]; |
| fakeTouchMove.phase = UITouchPhaseMoved; |
| |
| UITouch* fakeTouchEnd = [[UITouch alloc] init]; |
| fakeTouchEnd.phase = UITouchPhaseEnded; |
| |
| UITouch* fakeTouchCancelled = [[UITouch alloc] init]; |
| fakeTouchCancelled.phase = UITouchPhaseCancelled; |
| |
| [viewController |
| triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchBegan, nil]]; |
| XCTAssertFalse(link.isPaused); |
| |
| [viewController |
| triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, nil]]; |
| XCTAssertTrue(link.isPaused); |
| |
| [viewController |
| triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchMove, nil]]; |
| XCTAssertFalse(link.isPaused); |
| |
| [viewController |
| triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchCancelled, nil]]; |
| XCTAssertTrue(link.isPaused); |
| |
| [viewController |
| triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] |
| initWithObjects:fakeTouchBegan, fakeTouchEnd, nil]]; |
| XCTAssertFalse(link.isPaused); |
| |
| [viewController |
| triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] initWithObjects:fakeTouchEnd, |
| fakeTouchCancelled, nil]]; |
| XCTAssertTrue(link.isPaused); |
| |
| [viewController |
| triggerTouchRateCorrectionIfNeeded:[[NSSet alloc] |
| initWithObjects:fakeTouchMove, fakeTouchEnd, nil]]; |
| XCTAssertFalse(link.isPaused); |
| } |
| |
| - (void)testFlutterViewControllerStartKeyboardAnimationWillCreateVsyncClientCorrectly { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| viewController.targetViewInsetBottom = 100; |
| [viewController startKeyBoardAnimation:0.25]; |
| XCTAssertNotNil(viewController.keyboardAnimationVSyncClient); |
| } |
| |
| - (void) |
| testSetupKeyboardAnimationVsyncClientWillNotCreateNewVsyncClientWhenKeyboardAnimationCallbackIsNil { |
| FlutterEngine* engine = [[FlutterEngine alloc] init]; |
| [engine runWithEntrypoint:nil]; |
| FlutterViewController* viewController = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| [viewController setUpKeyboardAnimationVsyncClient:nil]; |
| XCTAssertNil(viewController.keyboardAnimationVSyncClient); |
| } |
| |
| @end |