// 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 #import #include "flutter/shell/platform/darwin/common/framework/Headers/FlutterMacros.h" #import "flutter/shell/platform/darwin/ios/framework/Headers/FlutterViewController.h" #include "FlutterBinaryMessenger.h" FLUTTER_ASSERT_ARC 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 - (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, *)) { 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, *)) { 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, *)) { 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, *)) { 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:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll currentOrientation:UIInterfaceOrientationPortraitUpsideDown didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAll currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown currentOrientation:UIInterfaceOrientationPortrait didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskAllButUpsideDown currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortrait currentOrientation:UIInterfaceOrientationPortrait didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskPortraitUpsideDown currentOrientation:UIInterfaceOrientationPortraitUpsideDown didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscape currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeLeft currentOrientation:UIInterfaceOrientationLandscapeLeft didChangeOrientation:NO resultingOrientation:0]; [self orientationTestWithOrientationUpdate:UIInterfaceOrientationMaskLandscapeRight currentOrientation:UIInterfaceOrientationLandscapeRight didChangeOrientation:NO resultingOrientation: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]; id observer = [[NSNotificationCenter defaultCenter] addObserverForName:FlutterViewControllerWillDealloc object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification* _Nonnull note) { [expectation fulfill]; }]; realVC = nil; } [self waitForExpectations:@[ expectation ] timeout:1.0]; } @end