// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#import "ios/chrome/browser/mini_map/ui_bundled/mini_map_coordinator.h"
#import "base/ios/block_types.h"
#import "base/ios/ios_util.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/test/task_environment.h"
#import "components/sync_preferences/testing_pref_service_syncable.h"
#import "ios/chrome/browser/mini_map/ui_bundled/mini_map_mediator.h"
#import "ios/chrome/browser/mini_map/ui_bundled/mini_map_mediator_delegate.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/prefs/browser_prefs.h"
#import "ios/chrome/browser/shared/model/prefs/pref_names.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/mini_map_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/test/providers/mini_map/test_mini_map.h"
#import "ios/chrome/test/scoped_key_window.h"
#import "ios/web/common/features.h"
#import "testing/gmock/include/gmock/gmock.h"
#import "testing/gtest/include/gtest/gtest.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
typedef void (^BlockWithViewController)(UIViewController*);
// Expose mediator to test coordinator.
@interface MiniMapCoordinator (Testing) <MiniMapMediatorDelegate>
// Override the mediator property
@property(nonatomic, strong) MiniMapMediator* mediator;
@end
// A Mini map factory tat return a mock version of the controller
@interface TestMiniMapControllerFactory : NSObject <MiniMapControllerFactory>
// Records the last address that has been passed to the factory
@property(nonatomic, copy) NSString* lastAddress;
// Records the last completion that has been passed to the factory
@property(nonatomic, copy) MiniMapControllerCompletion lastCompletion;
// The controller the factory will return.
@property(nonatomic, weak) id<MiniMapController> controller;
@end
@implementation TestMiniMapControllerFactory
- (id<MiniMapController>)
createMiniMapControllerForString:(NSString*)address
completion:(MiniMapControllerCompletion)completion {
_lastAddress = address;
_lastCompletion = completion;
return _controller;
}
@end
// Tests the MiniMapCoordinator logic and its links to the MiniMapController.
class MiniMapCoordinatorTest : public PlatformTest {
protected:
MiniMapCoordinatorTest() {
TestChromeBrowserState::Builder builder;
builder.SetPrefService(CreatePrefService());
browser_state_ = std::move(builder).Build();
browser_ = std::make_unique<TestBrowser>(browser_state_.get());
mock_application_command_handler_ =
OCMStrictProtocolMock(@protocol(ApplicationCommands));
mock_application_settings_command_handler_ =
OCMStrictProtocolMock(@protocol(SettingsCommands));
mock_mini_map_command_handler_ =
OCMStrictProtocolMock(@protocol(MiniMapCommands));
CommandDispatcher* dispatcher = browser_->GetCommandDispatcher();
[dispatcher startDispatchingToTarget:mock_application_command_handler_
forProtocol:@protocol(ApplicationCommands)];
[dispatcher
startDispatchingToTarget:mock_application_settings_command_handler_
forProtocol:@protocol(SettingsCommands)];
[dispatcher startDispatchingToTarget:mock_mini_map_command_handler_
forProtocol:@protocol(MiniMapCommands)];
root_view_controller_ = [[UIViewController alloc] init];
scoped_window_.Get().rootViewController = root_view_controller_;
factory_ = [[TestMiniMapControllerFactory alloc] init];
ios::provider::test::SetMiniMapControllerFactory(factory_);
}
void TearDown() override {
[coordinator_ stop];
EXPECT_OCMOCK_VERIFY(mock_application_command_handler_);
EXPECT_OCMOCK_VERIFY(mock_application_settings_command_handler_);
EXPECT_OCMOCK_VERIFY(mock_mini_map_command_handler_);
ios::provider::test::SetMiniMapControllerFactory(nil);
PlatformTest::TearDown();
}
void SetupCoordinator(BOOL consent_required, MiniMapMode type) {
coordinator_ = [[MiniMapCoordinator alloc]
initWithBaseViewController:root_view_controller_
browser:browser_.get()
webState:nullptr
text:@"Address"
consentRequired:consent_required
mode:type];
[coordinator_ start];
}
std::unique_ptr<sync_preferences::PrefServiceSyncable> CreatePrefService() {
auto prefs =
std::make_unique<sync_preferences::TestingPrefServiceSyncable>();
user_prefs::PrefRegistrySyncable* registry = prefs->registry();
RegisterBrowserStatePrefs(registry);
return prefs;
}
protected:
base::test::TaskEnvironment environment_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
id<MiniMapMediatorDelegate> delegate_;
std::unique_ptr<Browser> browser_;
MiniMapCoordinator* coordinator_;
TestMiniMapControllerFactory* factory_;
id mock_application_command_handler_;
id mock_application_settings_command_handler_;
id mock_mini_map_command_handler_;
ScopedKeyWindow scoped_window_;
UIViewController* root_view_controller_ = nil;
};
// Tests the map controller is start immediately if no consent is needed.
TEST_F(MiniMapCoordinatorTest, TestNoConsentNeededMap) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(NO, MiniMapMode::kMap);
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests the directions controller is start immediately if no consent is needed.
TEST_F(MiniMapCoordinatorTest, TestNoConsentNeededDirections) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
OCMExpect([mini_map_controller
presentDirectionsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(NO, MiniMapMode::kDirections);
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests that consent screen is triggered, then the map on consent.
TEST_F(MiniMapCoordinatorTest, TestShowMapAfterConsent) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(web::features::kOneTapForMaps);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesAccepted,
false);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesEnabled, true);
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
SetupCoordinator(YES, MiniMapMode::kMap);
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
__block BOOL called = NO;
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg checkWithBlock:^BOOL(
UIViewController*
view_controller) {
called = YES;
return YES;
}]]);
[coordinator_.mediator userConsented];
EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::test::ios::kWaitForUIElementTimeout, ^{
return called;
}));
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests that consent screen is not triggered after consent was given.
TEST_F(MiniMapCoordinatorTest, TestShowMapAfterConsentGiven) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(web::features::kOneTapForMaps);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesAccepted, true);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesEnabled, true);
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(YES, MiniMapMode::kMap);
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests that consent screen is not triggered, but IPH is configured.
TEST_F(MiniMapCoordinatorTest, TestIPH) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
base::test::ScopedFeatureList scoped_feature_list;
base::FieldTrialParams feature_parameters{
{web::features::kOneTapForMapsConsentModeParamTitle,
web::features::kOneTapForMapsConsentModeIPHParam}};
scoped_feature_list.InitAndEnableFeatureWithParameters(
web::features::kOneTapForMaps, feature_parameters);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesAccepted,
false);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesEnabled, true);
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
OCMExpect([mini_map_controller configureDisclaimerWithTitle:[OCMArg any]
subtitle:[OCMArg any]
actionHandler:[OCMArg any]]);
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(YES, MiniMapMode::kMap);
environment_.RunUntilIdle();
EXPECT_TRUE(
browser_state_->GetPrefs()->GetBoolean(prefs::kDetectAddressesAccepted));
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests IPH is not displayed on second trigger
TEST_F(MiniMapCoordinatorTest, TestIPHSecondLaunch) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
base::test::ScopedFeatureList scoped_feature_list;
base::FieldTrialParams feature_parameters{
{web::features::kOneTapForMapsConsentModeParamTitle,
web::features::kOneTapForMapsConsentModeIPHParam}};
scoped_feature_list.InitAndEnableFeatureWithParameters(
web::features::kOneTapForMaps, feature_parameters);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesAccepted, true);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesEnabled, true);
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(YES, MiniMapMode::kMap);
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests that correct metrics are logged on dismiss.
TEST_F(MiniMapCoordinatorTest, TestDismissMap) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
base::HistogramTester histogram_tester;
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(web::features::kOneTapForMaps);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesAccepted,
false);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesEnabled, true);
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(NO, MiniMapMode::kMap);
OCMExpect([mock_mini_map_command_handler_ hideMiniMap]);
factory_.lastCompletion(nil);
// Expect normal outcome.
histogram_tester.ExpectBucketCount("IOS.MiniMap.Outcome", 0, 1);
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests that URL is opened if requested on dismiss.
TEST_F(MiniMapCoordinatorTest, TestOpenURL) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
base::HistogramTester histogram_tester;
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(web::features::kOneTapForMaps);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesAccepted,
false);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesEnabled, true);
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
OCMExpect([mini_map_controller configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg any]
trailingButtonAction:[OCMArg any]]);
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(NO, MiniMapMode::kMap);
OCMExpect([mock_mini_map_command_handler_ hideMiniMap]);
OCMExpect([mock_application_command_handler_ openURLInNewTab:[OCMArg any]]);
factory_.lastCompletion([NSURL URLWithString:@"https://www.example.org"]);
// Expect url outcome.
histogram_tester.ExpectBucketCount("IOS.MiniMap.Outcome", 1, 1);
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}
// Tests the footer buttons.
TEST_F(MiniMapCoordinatorTest, TestFooterButtons) {
if (!base::ios::IsRunningOnOrLater(16, 4, 0)) {
GTEST_SKIP() << "Feature only available on iOS16.4+";
}
base::HistogramTester histogram_tester;
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(web::features::kOneTapForMaps);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesAccepted,
false);
browser_state_->GetPrefs()->SetBoolean(prefs::kDetectAddressesEnabled, true);
id mini_map_controller = OCMStrictProtocolMock(@protocol(MiniMapController));
factory_.controller = mini_map_controller;
__block BlockWithViewController left_button_block;
__block BlockWithViewController right_button_block;
OCMExpect([mini_map_controller
configureFooterWithTitle:[OCMArg any]
leadingButtonTitle:[OCMArg any]
trailingButtonTitle:[OCMArg any]
leadingButtonAction:[OCMArg checkWithBlock:^BOOL(
BlockWithViewController block) {
left_button_block = block;
return YES;
}]
trailingButtonAction:[OCMArg checkWithBlock:^BOOL(
BlockWithViewController block) {
right_button_block = block;
return YES;
}]]);
OCMExpect([mini_map_controller
presentMapsWithPresentingViewController:[OCMArg any]]);
SetupCoordinator(NO, MiniMapMode::kMap);
OCMExpect([mock_application_settings_command_handler_
showContentsSettingsFromViewController:[OCMArg any]]);
histogram_tester.ExpectBucketCount("IOS.MiniMap.Outcome", 3, 0);
left_button_block(nil);
histogram_tester.ExpectBucketCount("IOS.MiniMap.Outcome", 3, 1);
OCMExpect([mock_application_command_handler_
showReportAnIssueFromViewController:[OCMArg any]
sender:UserFeedbackSender::MiniMap]);
histogram_tester.ExpectBucketCount("IOS.MiniMap.Outcome", 2, 0);
right_button_block(nil);
histogram_tester.ExpectBucketCount("IOS.MiniMap.Outcome", 2, 1);
OCMExpect([mock_mini_map_command_handler_ hideMiniMap]);
factory_.lastCompletion(nil);
// Expect normal outcome.
histogram_tester.ExpectBucketCount("IOS.MiniMap.Outcome", 0, 1);
EXPECT_OCMOCK_VERIFY(mini_map_controller);
}