| // 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> |
| #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/FlutterViewController_Internal.h" |
| |
| #include "FlutterBinaryMessenger.h" |
| |
| FLUTTER_ASSERT_ARC |
| |
| @interface FlutterEngine () |
| - (BOOL)createShell:(NSString*)entrypoint libraryURI:(NSString*)libraryURI; |
| @end |
| |
| extern NSNotificationName const FlutterViewControllerWillDealloc; |
| |
| /// A simple mock class for FlutterEngine. |
| /// |
| /// OCMockClass 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. |
| @interface MockEngine : NSObject |
| @end |
| |
| @implementation MockEngine |
| - (FlutterViewController*)viewController { |
| return nil; |
| } |
| - (void)setViewController:(FlutterViewController*)viewController { |
| // noop |
| } |
| @end |
| |
| @interface FlutterViewControllerTest : XCTestCase |
| @end |
| |
| // The following conditional compilation defines an API 13 concept on earlier API targets so that |
| // a compiler compiling against API 12 or below does not blow up due to non-existent members. |
| #if __IPHONE_OS_VERSION_MAX_ALLOWED < 130000 |
| typedef enum UIAccessibilityContrast : NSInteger { |
| UIAccessibilityContrastUnspecified = 0, |
| UIAccessibilityContrastNormal = 1, |
| UIAccessibilityContrastHigh = 2 |
| } UIAccessibilityContrast; |
| |
| @interface UITraitCollection (MethodsFromNewerSDK) |
| - (UIAccessibilityContrast)accessibilityContrast; |
| @end |
| #endif |
| |
| @interface FlutterViewController (Tests) |
| - (void)performOrientationUpdate:(UIInterfaceOrientationMask)new_preferences; |
| @end |
| |
| @implementation FlutterViewControllerTest |
| |
| - (void)testBinaryMessenger { |
| id engine = OCMClassMock([FlutterEngine class]); |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertNotNil(vc); |
| id messenger = OCMProtocolMock(@protocol(FlutterBinaryMessenger)); |
| OCMStub([engine binaryMessenger]).andReturn(messenger); |
| XCTAssertEqual(vc.binaryMessenger, messenger); |
| OCMVerify([engine binaryMessenger]); |
| } |
| |
| #pragma mark - Platform Brightness |
| |
| - (void)testItReportsLightPlatformBrightnessByDefault { |
| // Setup test. |
| id engine = OCMClassMock([FlutterEngine class]); |
| |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([engine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine |
| 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 |
| [engine stopMocking]; |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsPlatformBrightnessWhenViewWillAppear { |
| // Setup test. |
| id engine = OCMClassMock([FlutterEngine class]); |
| |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([engine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine |
| 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 |
| [engine stopMocking]; |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsDarkPlatformBrightnessWhenTraitCollectionRequestsIt { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| id engine = OCMClassMock([FlutterEngine class]); |
| |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([engine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| 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(realVC); |
| 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]; |
| [engine 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 engine = OCMClassMock([FlutterEngine class]); |
| |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([engine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine |
| 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 |
| [engine stopMocking]; |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsPlatformContrastWhenViewWillAppear { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| id engine = OCMClassMock([FlutterEngine class]); |
| |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([engine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* vc = [[FlutterViewController alloc] initWithEngine:engine |
| 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 |
| [engine stopMocking]; |
| [settingsChannel stopMocking]; |
| } |
| |
| - (void)testItReportsHighContrastWhenTraitCollectionRequestsIt { |
| if (@available(iOS 13, *)) { |
| // noop |
| } else { |
| return; |
| } |
| |
| // Setup test. |
| id engine = OCMClassMock([FlutterEngine class]); |
| |
| id settingsChannel = OCMClassMock([FlutterBasicMessageChannel class]); |
| OCMStub([engine settingsChannel]).andReturn(settingsChannel); |
| |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| 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(realVC); |
| 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]; |
| [engine stopMocking]; |
| [settingsChannel stopMocking]; |
| [mockTraitCollection stopMocking]; |
| } |
| |
| - (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 engine = OCMClassMock([FlutterEngine class]); |
| |
| id deviceMock = OCMPartialMock([UIDevice currentDevice]); |
| if (!didChange) { |
| OCMReject([deviceMock setValue:[OCMArg any] forKey:@"orientation"]); |
| } else { |
| OCMExpect([deviceMock setValue:@(resultingOrientation) forKey:@"orientation"]); |
| } |
| |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| id mockApplication = OCMClassMock([UIApplication class]); |
| OCMStub([mockApplication sharedApplication]).andReturn(mockApplication); |
| OCMStub([mockApplication statusBarOrientation]).andReturn(currentOrientation); |
| |
| [realVC performOrientationUpdate:mask]; |
| OCMVerifyAll(deviceMock); |
| [engine stopMocking]; |
| [deviceMock stopMocking]; |
| [mockApplication stopMocking]; |
| } |
| |
| // 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 { |
| 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]; |
| }]; |
| realVC = nil; |
| } |
| [self waitForExpectations:@[ expectation ] timeout:1.0]; |
| } |
| |
| - (void)testDoesntLoadViewInInit { |
| FlutterDartProject* project = [[FlutterDartProject alloc] init]; |
| FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; |
| [engine createShell:@"" libraryURI:@""]; |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertFalse([realVC isViewLoaded], @"shouldn't have loaded since it hasn't been shown"); |
| } |
| |
| - (void)testHideOverlay { |
| FlutterDartProject* project = [[FlutterDartProject alloc] init]; |
| FlutterEngine* engine = [[FlutterEngine alloc] initWithName:@"foobar" project:project]; |
| [engine createShell:@"" libraryURI:@""]; |
| FlutterViewController* realVC = [[FlutterViewController alloc] initWithEngine:engine |
| nibName:nil |
| bundle:nil]; |
| XCTAssertFalse(realVC.prefersHomeIndicatorAutoHidden, @""); |
| [[NSNotificationCenter defaultCenter] postNotificationName:FlutterViewControllerHideHomeIndicator |
| object:nil]; |
| XCTAssertTrue(realVC.prefersHomeIndicatorAutoHidden, @""); |
| } |
| |
| @end |