// Copyright 2022 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/ui/popup_menu/overflow_menu/overflow_menu_mediator.h"
#import "base/files/scoped_temp_dir.h"
#import "base/ios/ios_util.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "base/time/default_clock.h"
#import "components/bookmarks/browser/bookmark_model.h"
#import "components/bookmarks/browser/bookmark_utils.h"
#import "components/bookmarks/common/bookmark_pref_names.h"
#import "components/bookmarks/test/bookmark_test_helpers.h"
#import "components/feature_engagement/test/mock_tracker.h"
#import "components/language/ios/browser/ios_language_detection_tab_helper.h"
#import "components/language/ios/browser/language_detection_java_script_feature.h"
#import "components/password_manager/core/browser/password_manager_test_utils.h"
#import "components/password_manager/core/browser/password_store/mock_password_store_interface.h"
#import "components/policy/core/common/mock_configuration_policy_provider.h"
#import "components/pref_registry/pref_registry_syncable.h"
#import "components/prefs/pref_registry_simple.h"
#import "components/prefs/testing_pref_service.h"
#import "components/reading_list/core/reading_list_model.h"
#import "components/signin/public/base/consent_level.h"
#import "components/signin/public/base/signin_metrics.h"
#import "components/signin/public/identity_manager/identity_manager.h"
#import "components/signin/public/identity_manager/identity_test_utils.h"
#import "components/supervised_user/core/browser/supervised_user_preferences.h"
#import "components/supervised_user/core/common/features.h"
#import "components/supervised_user/core/common/pref_names.h"
#import "components/supervised_user/test_support/supervised_user_signin_test_utils.h"
#import "components/sync/service/sync_service.h"
#import "components/sync/test/mock_sync_service.h"
#import "components/sync_preferences/pref_service_mock_factory.h"
#import "components/sync_preferences/pref_service_syncable.h"
#import "components/translate/core/browser/translate_pref_names.h"
#import "components/translate/core/browser/translate_prefs.h"
#import "components/translate/core/language_detection/language_detection_model.h"
#import "ios/chrome/browser/bookmarks/model/bookmark_model_factory.h"
#import "ios/chrome/browser/overlays/model/public/overlay_presenter.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request.h"
#import "ios/chrome/browser/overlays/model/public/overlay_request_queue.h"
#import "ios/chrome/browser/overlays/model/public/web_content_area/java_script_alert_dialog_overlay.h"
#import "ios/chrome/browser/overlays/model/test/fake_overlay_presentation_context.h"
#import "ios/chrome/browser/passwords/model/ios_chrome_profile_password_store_factory.h"
#import "ios/chrome/browser/policy/model/cloud/user_policy_constants.h"
#import "ios/chrome/browser/policy/model/enterprise_policy_test_helper.h"
#import "ios/chrome/browser/promos_manager/model/mock_promos_manager.h"
#import "ios/chrome/browser/reading_list/model/reading_list_model_factory.h"
#import "ios/chrome/browser/reading_list/model/reading_list_test_utils.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/model/application_context/application_context.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/model/web_state_list/web_state_list.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/shared/model/web_state_list/web_state_opener.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/signin/model/authentication_service.h"
#import "ios/chrome/browser/signin/model/authentication_service_factory.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service.h"
#import "ios/chrome/browser/signin/model/chrome_account_manager_service_factory.h"
#import "ios/chrome/browser/signin/model/fake_authentication_service_delegate.h"
#import "ios/chrome/browser/signin/model/fake_system_identity.h"
#import "ios/chrome/browser/signin/model/fake_system_identity_manager.h"
#import "ios/chrome/browser/signin/model/identity_manager_factory.h"
#import "ios/chrome/browser/signin/model/system_identity.h"
#import "ios/chrome/browser/signin/model/system_identity_manager.h"
#import "ios/chrome/browser/supervised_user/model/supervised_user_service_factory.h"
#import "ios/chrome/browser/ui/popup_menu//overflow_menu/overflow_menu_orderer.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/destination_usage_history/constants.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/feature_flags.h"
#import "ios/chrome/browser/ui/popup_menu/overflow_menu/overflow_menu_swift.h"
#import "ios/chrome/browser/ui/popup_menu/popup_menu_constants.h"
#import "ios/chrome/browser/ui/toolbar/test/toolbar_test_navigation_manager.h"
#import "ios/chrome/browser/ui/whats_new/constants.h"
#import "ios/chrome/browser/ui/whats_new/whats_new_util.h"
#import "ios/chrome/browser/web/model/font_size/font_size_java_script_feature.h"
#import "ios/chrome/browser/web/model/font_size/font_size_tab_helper.h"
#import "ios/chrome/test/ios_chrome_scoped_testing_local_state.h"
#import "ios/public/provider/chrome/browser/text_zoom/text_zoom_api.h"
#import "ios/public/provider/chrome/browser/user_feedback/user_feedback_api.h"
#import "ios/web/public/navigation/navigation_item.h"
#import "ios/web/public/test/fakes/fake_navigation_context.h"
#import "ios/web/public/test/fakes/fake_navigation_manager.h"
#import "ios/web/public/test/fakes/fake_web_frame.h"
#import "ios/web/public/test/fakes/fake_web_frames_manager.h"
#import "ios/web/public/test/fakes/fake_web_state.h"
#import "ios/web/public/test/js_test_util.h"
#import "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/web_state_observer_bridge.h"
#import "testing/gtest_mac.h"
#import "testing/platform_test.h"
#import "ui/base/device_form_factor.h"
using sync_preferences::PrefServiceMockFactory;
using sync_preferences::PrefServiceSyncable;
using testing::Return;
using user_prefs::PrefRegistrySyncable;
namespace {
const int kNumberOfWebStates = 3;
// Turns on Sync.
void SetupSyncServiceEnabledExpectations(
syncer::MockSyncService* sync_service) {
ON_CALL(*sync_service, GetTransportState())
.WillByDefault(Return(syncer::SyncService::TransportState::ACTIVE));
ON_CALL(*sync_service->GetMockUserSettings(),
IsInitialSyncFeatureSetupComplete())
.WillByDefault(Return(true));
ON_CALL(*sync_service->GetMockUserSettings(), GetSelectedTypes())
.WillByDefault(Return(syncer::UserSelectableTypeSet::All()));
ON_CALL(*sync_service, HasSyncConsent()).WillByDefault(Return(true));
}
// Sync Service error that is eligble to be indicated as an Identity error when
// Sync is turned OFF.
constexpr syncer::SyncService::UserActionableError
kEligibleIdentityErrorWhenSyncOff =
syncer::SyncService::UserActionableError::kNeedsPassphrase;
// Sync Service error that is ineligble to be indicated as an Identity error
// when Sync is turned OFF.
constexpr syncer::SyncService::UserActionableError
kIneligibleIdentityErrorWhenSyncOff =
syncer::SyncService::UserActionableError::kSignInNeedsUpdate;
void CleanupNSUserDefaults() {
[[NSUserDefaults standardUserDefaults]
removeObjectForKey:kWhatsNewM116UsageEntryKey];
}
// Creates a PrefService that can be used by the browser state.
std::unique_ptr<PrefServiceSyncable> CreatePrefServiceForBrowserState() {
PrefServiceMockFactory factory;
scoped_refptr<PrefRegistrySyncable> registry(new PrefRegistrySyncable);
std::unique_ptr<PrefServiceSyncable> prefs =
factory.CreateSyncable(registry.get());
RegisterBrowserStatePrefs(registry.get());
return prefs;
}
} // namespace
class OverflowMenuMediatorTest : public PlatformTest {
public:
OverflowMenuMediatorTest() : language_detection_model_(nullptr) {
pref_service_.registry()->RegisterBooleanPref(
translate::prefs::kOfferTranslateEnabled, true);
pref_service_.registry()->RegisterStringPref(prefs::kSupervisedUserId,
std::string());
pref_service_.registry()->RegisterBooleanPref(
prefs::kChildAccountStatusKnown, false);
}
void SetUp() override {
PlatformTest::SetUp();
// TODO(crbug.com/40260996): Removed this once the other test suites
// properly clean up their NSUserDefaults on teardown.
CleanupNSUserDefaults();
TestChromeBrowserState::Builder builder;
// Set a pref service for the ChromeBrowserState that is needed by some
// factories (e.g. AuthenticationServiceFactory). The browser prefs for
// testing the mediator are usually hosted in `browserStatePrefs_`.
builder.SetPrefService(CreatePrefServiceForBrowserState());
builder.AddTestingFactory(
ios::TemplateURLServiceFactory::GetInstance(),
ios::TemplateURLServiceFactory::GetDefaultFactory());
builder.AddTestingFactory(ios::BookmarkModelFactory::GetInstance(),
ios::BookmarkModelFactory::GetDefaultFactory());
builder.AddTestingFactory(
IOSChromeProfilePasswordStoreFactory::GetInstance(),
base::BindRepeating(&password_manager::BuildPasswordStoreInterface<
web::BrowserState,
password_manager::MockPasswordStoreInterface>));
builder.AddTestingFactory(
ReadingListModelFactory::GetInstance(),
base::BindRepeating(&BuildReadingListModelWithFakeStorage,
std::vector<scoped_refptr<ReadingListEntry>>()));
builder.AddTestingFactory(
AuthenticationServiceFactory::GetInstance(),
AuthenticationServiceFactory::GetDefaultFactory());
browser_state_ = std::move(builder).Build();
AuthenticationServiceFactory::CreateAndInitializeForBrowserState(
browser_state_.get(),
std::make_unique<FakeAuthenticationServiceDelegate>());
web::test::OverrideJavaScriptFeatures(
browser_state_.get(),
{language::LanguageDetectionJavaScriptFeature::GetInstance()});
// Set up the TestBrowser.
browser_ = std::make_unique<TestBrowser>(browser_state_.get());
// Set up the WebStateList.
auto navigation_manager = std::make_unique<ToolbarTestNavigationManager>();
navigation_item_ = web::NavigationItem::Create();
GURL url = GURL("http://chromium.org");
navigation_item_->SetURL(url);
navigation_manager->SetVisibleItem(navigation_item_.get());
std::unique_ptr<web::FakeWebState> test_web_state =
std::make_unique<web::FakeWebState>();
test_web_state->SetNavigationManager(std::move(navigation_manager));
test_web_state->SetLoading(true);
test_web_state->SetBrowserState(browser_state_.get());
web_state_ = test_web_state.get();
auto frames_manager = std::make_unique<web::FakeWebFramesManager>();
auto main_frame = web::FakeWebFrame::CreateMainWebFrame(
/*security_origin=*/url);
main_frame->set_browser_state(browser_state_.get());
frames_manager->AddWebFrame(std::move(main_frame));
web::ContentWorld content_world =
language::LanguageDetectionJavaScriptFeature::GetInstance()
->GetSupportedContentWorld();
web_state_->SetWebFramesManager(content_world, std::move(frames_manager));
browser_->GetWebStateList()->InsertWebState(
std::move(test_web_state), WebStateList::InsertionParams::AtIndex(0));
for (int i = 1; i < kNumberOfWebStates; i++) {
InsertNewWebState(i);
}
// Set up the OverlayPresenter.
OverlayPresenter::FromBrowser(browser_.get(),
OverlayModality::kWebContentArea)
->SetPresentationContext(&presentation_context_);
baseViewController_ = [[UIViewController alloc] init];
model_ = [[OverflowMenuModel alloc] initWithDestinations:@[]
actionGroups:@[]];
}
void TearDown() override {
// Explicitly disconnect the mediator so there won't be any WebStateList
// observers when browser_ gets destroyed.
[mediator_ disconnect];
browser_.reset();
CleanupNSUserDefaults();
PlatformTest::TearDown();
}
protected:
OverflowMenuMediator* CreateMediator(BOOL is_incognito) {
orderer_ = [[OverflowMenuOrderer alloc] initWithIsIncognito:is_incognito];
orderer_.model = model_;
mediator_ = [[OverflowMenuMediator alloc] init];
mediator_.isIncognito = is_incognito;
mediator_.menuOrderer = orderer_;
mediator_.baseViewController = baseViewController_;
SetUpReadingList();
return mediator_;
}
OverflowMenuMediator* CreateMediatorWithBrowserPolicyConnector(
BOOL is_incognito,
BrowserPolicyConnectorIOS* browser_policy_connector) {
CreateMediator(is_incognito);
mediator_.browserPolicyConnector = browser_policy_connector;
return mediator_;
}
void CreateBrowserStatePrefs() {
browserStatePrefs_ = std::make_unique<TestingPrefServiceSimple>();
browserStatePrefs_->registry()->RegisterBooleanPref(
bookmarks::prefs::kEditBookmarksEnabled,
/*default_value=*/true);
}
void CreateLocalStatePrefs() {
localStatePrefs_ = std::make_unique<TestingPrefServiceSimple>();
localStatePrefs_->registry()->RegisterListPref(
prefs::kOverflowMenuNewDestinations, PrefRegistry::LOSSY_PREF);
localStatePrefs_->registry()->RegisterDictionaryPref(
prefs::kOverflowMenuDestinationUsageHistory, PrefRegistry::LOSSY_PREF);
localStatePrefs_->registry()->RegisterListPref(
prefs::kOverflowMenuDestinationsOrder);
localStatePrefs_->registry()->RegisterDictionaryPref(
prefs::kOverflowMenuActionsOrder);
}
void SetUpBookmarks() {
bookmark_model_ =
ios::BookmarkModelFactory::GetForBrowserState(browser_state_.get());
DCHECK(bookmark_model_);
bookmarks::test::WaitForBookmarkModelToLoad(bookmark_model_);
mediator_.bookmarkModel = bookmark_model_;
}
void SetUpReadingList() {
reading_list_model_ =
ReadingListModelFactory::GetForBrowserState(browser_state_.get());
DCHECK(reading_list_model_);
ASSERT_TRUE(
base::test::ios::WaitUntilConditionOrTimeout(base::Seconds(5), ^{
return reading_list_model_->loaded();
}));
mediator_.readingListModel = reading_list_model_;
}
void InsertNewWebState(int index) {
auto web_state = std::make_unique<web::FakeWebState>();
GURL url("http://test/" + base::NumberToString(index));
web_state->SetCurrentURL(url);
auto frames_manager = std::make_unique<web::FakeWebFramesManager>();
auto main_frame = web::FakeWebFrame::CreateMainWebFrame(
/*security_origin=*/url);
main_frame->set_browser_state(browser_state_.get());
frames_manager->AddWebFrame(std::move(main_frame));
web::ContentWorld content_world =
language::LanguageDetectionJavaScriptFeature::GetInstance()
->GetSupportedContentWorld();
web_state->SetWebFramesManager(content_world, std::move(frames_manager));
browser_->GetWebStateList()->InsertWebState(
std::move(web_state), WebStateList::InsertionParams::AtIndex(index));
}
void SetUpActiveWebState() {
// OverflowMenuMediator expects an language::IOSLanguageDetectionTabHelper
// for the currently active WebState.
language::IOSLanguageDetectionTabHelper::CreateForWebState(
browser_->GetWebStateList()->GetWebStateAt(0),
/*url_language_histogram=*/nullptr, &language_detection_model_,
&pref_service_);
browser_->GetWebStateList()->ActivateWebStateAt(0);
}
// Checks that the overflowMenuModel is receiving a number of destination
// items corresponding to `destination_items` and the action group number
// corresponding to `action_items` content.
void CheckMediatorSetItems(NSUInteger destination_items,
NSArray<NSNumber*>* action_items) {
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
OverflowMenuModel* model = mediator_.model;
EXPECT_EQ(destination_items, model.destinations.count);
EXPECT_EQ(action_items.count, model.actionGroups.count);
for (NSUInteger index = 0; index < action_items.count; index++) {
NSNumber* expected_count = action_items[index];
EXPECT_EQ(expected_count.unsignedIntValue,
model.actionGroups[index].actions.count);
}
}
bool HasItem(NSString* accessibility_identifier, BOOL enabled) {
for (OverflowMenuDestination* destination in mediator_.model.destinations) {
if (destination.accessibilityIdentifier == accessibility_identifier)
return YES;
}
for (OverflowMenuActionGroup* group in mediator_.model.actionGroups) {
for (OverflowMenuAction* action in group.actions) {
if (action.accessibilityIdentifier == accessibility_identifier)
return action.enabled == enabled;
}
}
return NO;
}
bool HasEnterpriseInfoItem() {
for (OverflowMenuActionGroup* group in mediator_.model.actionGroups) {
if (group.footer.accessibilityIdentifier == kTextMenuEnterpriseInfo)
return YES;
}
return NO;
}
bool HasFamilyLinkInfoItem() {
for (OverflowMenuActionGroup* group in mediator_.model.actionGroups) {
if (group.footer.accessibilityIdentifier == kTextMenuFamilyLinkInfo) {
return YES;
}
}
return NO;
}
OverflowMenuDestination* GetDestination(NSString* accessibility_identifier) {
OverflowMenuDestination* found_destination = nil;
for (OverflowMenuDestination* destination in mediator_.model.destinations) {
if (destination.accessibilityIdentifier == accessibility_identifier) {
EXPECT_EQ(nil, found_destination)
<< "there shouldn't be more than one destination with the \""
<< accessibility_identifier << "\" accessibility identifier";
found_destination = destination;
}
}
return found_destination;
}
signin::IdentityManager* identity_manager() {
return IdentityManagerFactory::GetForBrowserState(browser_state_.get());
}
FakeSystemIdentityManager* fake_system_identity_manager() {
return FakeSystemIdentityManager::FromSystemIdentityManager(
GetApplicationContext()->GetSystemIdentityManager());
}
void SignInPrimaryAccountWithSupervisionStatus(bool is_supervised) {
const FakeSystemIdentity* identity = [FakeSystemIdentity fakeIdentity1];
fake_system_identity_manager()->AddIdentityWithUnknownCapabilities(
identity);
AuthenticationServiceFactory::GetForBrowserState(browser_state_.get())
->SignIn(identity, signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN);
CoreAccountInfo core_account_info =
identity_manager()->GetPrimaryAccountInfo(
signin::ConsentLevel::kSignin);
AccountInfo account =
identity_manager()->FindExtendedAccountInfo(core_account_info);
supervised_user::UpdateSupervisionStatusForAccount(
account, identity_manager(), is_supervised);
}
web::WebTaskEnvironment task_env_;
// Set a local state for the test ApplicationContext that is scoped to the
// test (cleaned up on teardown). This is needed for certains factories that
// gets the local state from the ApplicationContext directly, e.g. the
// AuthenticationServiceFactory. Valid local state prefs for testing the
// mediator are usually hosted in `localStatePrefs_`.
IOSChromeScopedTestingLocalState scoped_testing_local_state_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
std::unique_ptr<Browser> browser_;
FakeOverlayPresentationContext presentation_context_;
OverflowMenuModel* model_;
OverflowMenuMediator* mediator_;
OverflowMenuOrderer* orderer_;
raw_ptr<bookmarks::BookmarkModel> bookmark_model_;
raw_ptr<ReadingListModel> reading_list_model_;
std::unique_ptr<TestingPrefServiceSimple> browserStatePrefs_;
std::unique_ptr<TestingPrefServiceSimple> localStatePrefs_;
raw_ptr<web::FakeWebState> web_state_;
std::unique_ptr<web::NavigationItem> navigation_item_;
UIViewController* baseViewController_;
translate::LanguageDetectionModel language_detection_model_;
TestingPrefServiceSimple pref_service_;
feature_engagement::test::MockTracker tracker_;
};
// Tests that the feature engagement tracker get notified when the mediator is
// disconnected and the tracker wants the notification badge displayed.
TEST_F(OverflowMenuMediatorTest, TestFeatureEngagementDisconnect) {
CreateMediator(/*is_incognito=*/NO);
EXPECT_CALL(tracker_, ShouldTriggerHelpUI(testing::_))
.WillRepeatedly(Return(true));
mediator_.engagementTracker = &tracker_;
// Force model update.
mediator_.model = model_;
// There may be one or more Tools Menu items that use engagement trackers.
EXPECT_CALL(tracker_, Dismissed(testing::_)).Times(testing::AtLeast(1));
}
// Tests that the mediator is returning the right number of items and sections
// for the Tools Menu type.
TEST_F(OverflowMenuMediatorTest, TestMenuItemsCount) {
CreateLocalStatePrefs();
CreateMediator(/*is_incognito=*/NO);
mediator_.localStatePrefs = localStatePrefs_.get();
mediator_.model = model_;
NSUInteger number_of_action_items = 6;
if (ios::provider::IsTextZoomEnabled()) {
number_of_action_items++;
}
// New Tab, New Incognito Tab.
NSUInteger number_of_tab_actions = 2;
if (IsSplitToolbarMode(mediator_.baseViewController)) {
// Stop/Reload only shows in split toolbar mode.
number_of_tab_actions++;
}
if (base::ios::IsMultipleScenesSupported()) {
// New Window option is added in this case.
number_of_tab_actions++;
}
NSUInteger number_of_help_items = 2;
if (ios::provider::IsUserFeedbackSupported()) {
number_of_help_items++;
}
// Checks that Tools Menu has the right number of items in each section.
CheckMediatorSetItems(9, @[
@(number_of_tab_actions),
// Other actions, depending on configuration.
@(number_of_action_items),
// Customization button
@(1),
// Feedback/help actions.
@(number_of_help_items),
]);
}
// Tests that the items returned by the mediator are correctly enabled on a
// WebPage.
TEST_F(OverflowMenuMediatorTest, TestItemsStatusOnWebPage) {
CreateLocalStatePrefs();
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
mediator_.localStatePrefs = localStatePrefs_.get();
// Force model update.
mediator_.model = model_;
web::FakeNavigationContext context;
web_state_->OnNavigationFinished(&context);
EXPECT_TRUE(HasItem(kToolsMenuNewTabId, /*enabled=*/YES));
EXPECT_TRUE(HasItem(kToolsMenuSiteInformation, /*enabled=*/YES));
}
// Tests that the items returned by the mediator are correctly enabled on the
// NTP.
TEST_F(OverflowMenuMediatorTest, TestItemsStatusOnNTP) {
CreateLocalStatePrefs();
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
mediator_.localStatePrefs = localStatePrefs_.get();
// Force model update.
mediator_.model = model_;
navigation_item_->SetURL(GURL("chrome://newtab"));
web::FakeNavigationContext context;
web_state_->OnNavigationFinished(&context);
EXPECT_TRUE(HasItem(kToolsMenuNewTabId, /*enabled=*/YES));
EXPECT_FALSE(HasItem(kToolsMenuSiteInformation, /*enabled=*/YES));
}
// Tests that the "Add to Reading List" button is disabled while overlay UI is
// displayed in OverlayModality::kWebContentArea.
TEST_F(OverflowMenuMediatorTest, TestReadLaterDisabled) {
const GURL kUrl("https://chromium.test");
web_state_->SetCurrentURL(kUrl);
CreateBrowserStatePrefs();
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
mediator_.webContentAreaOverlayPresenter = OverlayPresenter::FromBrowser(
browser_.get(), OverlayModality::kWebContentArea);
mediator_.browserStatePrefs = browserStatePrefs_.get();
// Force model update.
mediator_.model = model_;
ASSERT_TRUE(HasItem(kToolsMenuReadLater, /*enabled=*/YES));
// Present a JavaScript alert over the WebState and verify that the page is no
// longer shareable.
OverlayRequestQueue* queue = OverlayRequestQueue::FromWebState(
web_state_, OverlayModality::kWebContentArea);
queue->AddRequest(
OverlayRequest::CreateWithConfig<JavaScriptAlertDialogRequest>(
web_state_, kUrl,
/*is_main_frame=*/true, @"message"));
EXPECT_TRUE(HasItem(kToolsMenuReadLater, /*enabled=*/NO));
// Cancel the request and verify that the "Add to Reading List" button is
// enabled.
queue->CancelAllRequests();
EXPECT_TRUE(HasItem(kToolsMenuReadLater, /*enabled=*/YES));
}
// Tests that the "Text Zoom..." button is disabled on non-HTML pages.
TEST_F(OverflowMenuMediatorTest, TestTextZoomDisabled) {
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
// FontSizeTabHelper requires a web frames manager.
web_state_->SetWebFramesManager(
FontSizeJavaScriptFeature::GetInstance()->GetSupportedContentWorld(),
std::make_unique<web::FakeWebFramesManager>());
FontSizeTabHelper::CreateForWebState(
browser_->GetWebStateList()->GetWebStateAt(0));
// Force model update.
mediator_.model = model_;
EXPECT_TRUE(HasItem(kToolsMenuTextZoom, /*enabled=*/YES));
web_state_->SetContentIsHTML(false);
// Fake a navigationFinished to force the popup menu items to update.
web::FakeNavigationContext context;
web_state_->OnNavigationFinished(&context);
EXPECT_TRUE(HasItem(kToolsMenuTextZoom, /*enabled=*/NO));
}
// Tests that the "Managed by..." item is hidden when none of the policies is
// set.
TEST_F(OverflowMenuMediatorTest, TestEnterpriseInfoHidden) {
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
// Force model update.
mediator_.model = model_;
ASSERT_FALSE(HasEnterpriseInfoItem());
}
// Tests that the "Managed by..." item is shown for user level policies when
// the UserPolicy features is enabled and the browser is signed in with a
// managed account.
TEST_F(OverflowMenuMediatorTest, TestEnterpriseInfoShownForUserLevelPolicies) {
// Enable the UserPolicy feature.
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitWithFeatures(
/*enabled_features=*/{policy::kUserPolicyForSigninOrSyncConsentLevel},
{});
// Add managed account to sign in with.
FakeSystemIdentity* fake_system_identity =
[FakeSystemIdentity fakeManagedIdentity];
fake_system_identity_manager()->AddIdentity(fake_system_identity);
// Emulate signing in with managed account.
AuthenticationService* authentication_service =
AuthenticationServiceFactory::GetForBrowserState(browser_state_.get());
ChromeAccountManagerService* account_manager =
ChromeAccountManagerServiceFactory::GetForBrowserState(
browser_state_.get());
authentication_service->SignIn(
account_manager->GetDefaultIdentity(),
signin_metrics::AccessPoint::ACCESS_POINT_UNKNOWN);
EXPECT_TRUE(authentication_service->HasPrimaryIdentityManaged(
signin::ConsentLevel::kSignin));
CreateMediator(/*is_incognito=*/NO);
// Set the objects needed to detect the signed in managed account.
mediator_.authenticationService =
AuthenticationServiceFactory::GetForBrowserState(browser_state_.get());
mediator_.browserStatePrefs = browser_state_->GetPrefs();
// Force model update.
mediator_.model = model_;
ASSERT_TRUE(HasEnterpriseInfoItem());
}
// Tests that the "Managed by..." item is shown for machine level policies
// (e.g. from MDM or CBCM).
TEST_F(OverflowMenuMediatorTest,
TestEnterpriseInfoShownForMachineLevelPolicies) {
// Set a policy.
base::ScopedTempDir state_directory;
ASSERT_TRUE(state_directory.CreateUniqueTempDir());
std::unique_ptr<EnterprisePolicyTestHelper> enterprise_policy_helper =
std::make_unique<EnterprisePolicyTestHelper>(state_directory.GetPath());
BrowserPolicyConnectorIOS* connector =
enterprise_policy_helper->GetBrowserPolicyConnector();
policy::PolicyMap map;
map.Set("test-policy", policy::POLICY_LEVEL_MANDATORY,
policy::POLICY_SCOPE_USER, policy::POLICY_SOURCE_PLATFORM,
base::Value("hello"), nullptr);
enterprise_policy_helper->GetPolicyProvider()->UpdateChromePolicy(map);
CreateMediatorWithBrowserPolicyConnector(
/*is_incognito=*/NO, connector);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
// Force model update.
mediator_.model = model_;
ASSERT_TRUE(HasEnterpriseInfoItem());
}
// Tests that the Family Link item is hidden for non-supervised users.
TEST_F(OverflowMenuMediatorTest, TestFamilyLinkInfoHidden) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
supervised_user::kReplaceSupervisionPrefsWithAccountCapabilitiesOnIOS);
// Sign in unsupervised user.
SignInPrimaryAccountWithSupervisionStatus(/*is_supervised=*/false);
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
// Force model update.
mediator_.model = model_;
ASSERT_FALSE(HasFamilyLinkInfoItem());
}
// Tests that the Family Link item is hidden for non-supervised users with
// pref-based supervision status.
TEST_F(OverflowMenuMediatorTest, TestFamilyLinkInfoHiddenWithSupervisionPrefs) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
supervised_user::kReplaceSupervisionPrefsWithAccountCapabilitiesOnIOS);
supervised_user::DisableParentalControls(*browser_state_->GetPrefs());
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
// Force model update.
mediator_.model = model_;
ASSERT_FALSE(HasFamilyLinkInfoItem());
}
// Tests that the Family Link item is shown for supervised users.
TEST_F(OverflowMenuMediatorTest, TestFamilyLinkInfoShown) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(
supervised_user::kReplaceSupervisionPrefsWithAccountCapabilitiesOnIOS);
// Sign in supervised user.
SignInPrimaryAccountWithSupervisionStatus(/*is_supervised=*/true);
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
// Force model update.
mediator_.model = model_;
ASSERT_TRUE(HasFamilyLinkInfoItem());
}
// Tests that the Family Link item is shown for supervised users with pref-based
// supervision status.
TEST_F(OverflowMenuMediatorTest, TestFamilyLinkInfoShownWithSupervisionPrefs) {
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndDisableFeature(
supervised_user::kReplaceSupervisionPrefsWithAccountCapabilitiesOnIOS);
supervised_user::EnableParentalControls(*browser_state_->GetPrefs());
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
// Force model update.
mediator_.model = model_;
ASSERT_TRUE(HasFamilyLinkInfoItem());
}
// Tests that 1) the tools menu has an enabled 'Add to Bookmarks' button when
// the current URL is not in bookmarks 2) the bookmark button changes to an
// enabled 'Edit bookmark' button when navigating to a bookmarked URL, 3) the
// bookmark button changes to 'Add to Bookmarks' when the bookmark is removed.
TEST_F(OverflowMenuMediatorTest, TestBookmarksToolsMenuButtons) {
const GURL nonBookmarkedURL("https://nonbookmarked.url");
const GURL bookmarkedURL("https://bookmarked.url");
web_state_->SetCurrentURL(nonBookmarkedURL);
SetUpActiveWebState();
CreateMediator(/*is_incognito=*/NO);
CreateBrowserStatePrefs();
SetUpBookmarks();
bookmark_model_->AddURL(bookmark_model_->mobile_node(), 0,
base::SysNSStringToUTF16(@"Test bookmark"),
bookmarkedURL);
mediator_.webStateList = browser_->GetWebStateList();
mediator_.browserStatePrefs = browserStatePrefs_.get();
// Force model update.
mediator_.model = model_;
EXPECT_TRUE(HasItem(kToolsMenuAddToBookmarks, /*enabled=*/YES));
// Navigate to bookmarked url
web_state_->SetCurrentURL(bookmarkedURL);
web::FakeNavigationContext context;
web_state_->OnNavigationFinished(&context);
EXPECT_FALSE(HasItem(kToolsMenuAddToBookmarks, /*enabled=*/YES));
EXPECT_TRUE(HasItem(kToolsMenuEditBookmark, /*enabled=*/YES));
ios::BookmarkModelFactory::GetForBrowserState(browser_state_.get())
->RemoveAllUserBookmarks(FROM_HERE);
EXPECT_TRUE(HasItem(kToolsMenuAddToBookmarks, /*enabled=*/YES));
EXPECT_FALSE(HasItem(kToolsMenuEditBookmark, /*enabled=*/YES));
}
// Tests that the bookmark button is disabled when EditBookmarksEnabled pref is
// changed to false.
TEST_F(OverflowMenuMediatorTest, TestDisableBookmarksButton) {
const GURL url("https://chromium.test");
web_state_->SetCurrentURL(url);
SetUpActiveWebState();
CreateMediator(/*is_incognito=*/NO);
CreateBrowserStatePrefs();
mediator_.webStateList = browser_->GetWebStateList();
mediator_.browserStatePrefs = browserStatePrefs_.get();
// Force model update.
mediator_.model = model_;
EXPECT_TRUE(HasItem(kToolsMenuAddToBookmarks, /*enabled=*/YES));
browserStatePrefs_->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled,
false);
EXPECT_TRUE(HasItem(kToolsMenuAddToBookmarks, /*enabled=*/NO));
}
// Tests that WhatsNew destination was added to the OverflowMenuModel when
// What's New is enabled.
TEST_F(OverflowMenuMediatorTest, TestWhatsNewEnabled) {
const GURL kUrl("https://chromium.test");
web_state_->SetCurrentURL(kUrl);
CreateBrowserStatePrefs();
CreateLocalStatePrefs();
CreateMediator(/*is_incognito=*/NO);
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
mediator_.webContentAreaOverlayPresenter = OverlayPresenter::FromBrowser(
browser_.get(), OverlayModality::kWebContentArea);
mediator_.browserStatePrefs = browserStatePrefs_.get();
mediator_.localStatePrefs = localStatePrefs_.get();
// Force model update.
mediator_.model = model_;
EXPECT_TRUE(HasItem(kToolsMenuWhatsNewId, /*enabled=*/YES));
}
// This tests that crbug.com/1404673 does not regress. It isn't perfect, as
// that bug was never reproduced, but it tests part of the issue.
TEST_F(OverflowMenuMediatorTest, TestOpenWhatsNewDoesntCrashWithNoTracker) {
// Create Mediator and DO NOT set the Tracker on it.
CreateMediator(/*is_incognito=*/NO);
// Force model update.
mediator_.model = model_;
// Find the What's New destination.
OverflowMenuDestination* whatsNewDestination;
for (OverflowMenuDestination* destination in mediator_.model.destinations) {
if (destination.accessibilityIdentifier == kToolsMenuWhatsNewId) {
whatsNewDestination = destination;
break;
}
}
EXPECT_NSNE(nil, whatsNewDestination);
// Call What's New Destination's handler, which should not crash.
whatsNewDestination.handler();
}
// Tests that the Settings destination is badged with an error dot and
// positioned at at most kNewDestinationsInsertionIndex when there is an
// eligible identity error that can be resolved from the Settings menu.
TEST_F(OverflowMenuMediatorTest, TestEligibleIdentityErrorWhenSyncOff) {
CreateMediator(/*is_incognito=*/NO);
syncer::MockSyncService syncService;
// Inject eligible identity error in Sync Service.
ON_CALL(syncService, GetUserActionableError())
.WillByDefault(Return(kEligibleIdentityErrorWhenSyncOff));
mediator_.syncService = &syncService;
CreateLocalStatePrefs();
mediator_.localStatePrefs = localStatePrefs_.get();
mediator_.model = model_;
// Verify that the Settings destination is put at
// the kNewDestinationsInsertionIndex position and that it has the error
// badge to indicate the error.
OverflowMenuDestination* promotedDestination =
mediator_.model.destinations[kNewDestinationsInsertionIndex];
EXPECT_NSEQ(kToolsMenuSettingsId,
promotedDestination.accessibilityIdentifier);
EXPECT_EQ(BadgeTypeError, promotedDestination.badge);
}
// Tests that there is no error badge displayed on the Settings destination when
// there is no eligible identity error. Sync is OFF.
TEST_F(OverflowMenuMediatorTest, TestNoEligibleIdentityErrorWhenSyncOff) {
CreateMediator(/*is_incognito=*/NO);
syncer::MockSyncService syncService;
ON_CALL(syncService, GetUserActionableError())
.WillByDefault(Return(kIneligibleIdentityErrorWhenSyncOff));
mediator_.syncService = &syncService;
CreateLocalStatePrefs();
mediator_.localStatePrefs = localStatePrefs_.get();
mediator_.model = model_;
// Verify that the Settings destination it still there and does not have the
// error badge.
OverflowMenuDestination* settingsDestination =
GetDestination(kToolsMenuSettingsId);
ASSERT_NE(nil, settingsDestination);
EXPECT_EQ(BadgeTypeNone, settingsDestination.badge);
}
// Tests that there is an error badge on the Settings destination when there is
// a Sync error that will be indicated in the Settings menu. The account is
// signed in and has Sync turned ON.
TEST_F(OverflowMenuMediatorTest, TestSyncError) {
CreateMediator(/*is_incognito=*/NO);
syncer::MockSyncService syncService;
// Inject Sync error in Sync Service.
ON_CALL(syncService, GetUserActionableError())
.WillByDefault(Return(kIneligibleIdentityErrorWhenSyncOff));
SetupSyncServiceEnabledExpectations(&syncService);
mediator_.syncService = &syncService;
CreateLocalStatePrefs();
mediator_.localStatePrefs = localStatePrefs_.get();
mediator_.model = model_;
// Verify that the Settings destination is put at the front of the
// destinations and that it has the red dot badge to indicate the error.
OverflowMenuDestination* promotedDestination =
mediator_.model.destinations[kNewDestinationsInsertionIndex];
EXPECT_NSEQ(kToolsMenuSettingsId,
promotedDestination.accessibilityIdentifier);
EXPECT_EQ(BadgeTypeError, promotedDestination.badge);
}
// Tests that there is no error cue (red dot) displayed on the Settings
// destination when there is no error in both Sync and Identity levels.
TEST_F(OverflowMenuMediatorTest, TestNoSyncError) {
CreateMediator(/*is_incognito=*/NO);
syncer::MockSyncService syncService;
ON_CALL(syncService, GetUserActionableError())
.WillByDefault(Return(syncer::SyncService::UserActionableError::kNone));
SetupSyncServiceEnabledExpectations(&syncService);
mediator_.syncService = &syncService;
CreateLocalStatePrefs();
mediator_.localStatePrefs = localStatePrefs_.get();
mediator_.model = model_;
// Verify that the Settings destination it still there and does not have the
// error badge.
OverflowMenuDestination* settingsDestination =
GetDestination(kToolsMenuSettingsId);
ASSERT_NE(nil, settingsDestination);
EXPECT_EQ(BadgeTypeNone, settingsDestination.badge);
}
// Tests that the Settings destination that has an error cue has predence over
// the promoted What's New destination.
TEST_F(OverflowMenuMediatorTest, TestIdentityErrorWithWhatsNewPromo) {
const GURL kUrl("https://chromium.test");
web_state_->SetCurrentURL(kUrl);
CreateBrowserStatePrefs();
CreateMediator(/*is_incognito=*/NO);
// Show the new label badge for What's New.
ON_CALL(tracker_, ShouldTriggerHelpUI(testing::Ref(
feature_engagement::kIPHWhatsNewUpdatedFeature)))
.WillByDefault(testing::Return(true));
mediator_.engagementTracker = &tracker_;
SetUpActiveWebState();
mediator_.webStateList = browser_->GetWebStateList();
mediator_.webContentAreaOverlayPresenter = OverlayPresenter::FromBrowser(
browser_.get(), OverlayModality::kWebContentArea);
mediator_.browserStatePrefs = browserStatePrefs_.get();
CreateLocalStatePrefs();
mediator_.localStatePrefs = localStatePrefs_.get();
syncer::MockSyncService syncService;
ON_CALL(syncService, GetUserActionableError())
.WillByDefault(
Return(syncer::SyncService::UserActionableError::kNeedsPassphrase));
mediator_.syncService = &syncService;
mediator_.model = model_;
// Verify that the Settings destination is put at the front of the
// destinations and that What's New is put at the second place.
EXPECT_NSEQ(kToolsMenuSettingsId,
mediator_.model.destinations[kNewDestinationsInsertionIndex]
.accessibilityIdentifier);
EXPECT_NSEQ(kToolsMenuWhatsNewId,
mediator_.model.destinations[kNewDestinationsInsertionIndex + 1]
.accessibilityIdentifier);
}
// Tests that there is blue dot displayed on the Settings destination when there
// is no sync error.
TEST_F(OverflowMenuMediatorTest, TestSettingsBlueDotBadge) {
CreateMediator(/*is_incognito=*/NO);
syncer::MockSyncService syncService;
ON_CALL(syncService, GetUserActionableError())
.WillByDefault(Return(syncer::SyncService::UserActionableError::kNone));
SetupSyncServiceEnabledExpectations(&syncService);
mediator_.syncService = &syncService;
CreateLocalStatePrefs();
mediator_.localStatePrefs = localStatePrefs_.get();
mediator_.hasSettingsBlueDot = YES;
mediator_.model = model_;
// Verify that the Settings destination is there and has the promo badge.
OverflowMenuDestination* promotedDestination =
mediator_.model.destinations[kNewDestinationsInsertionIndex];
EXPECT_NSEQ(kToolsMenuSettingsId,
promotedDestination.accessibilityIdentifier);
EXPECT_EQ(BadgeTypePromo, promotedDestination.badge);
}
// Tests that the destinations are still promoted when there is no usage
// history ranking.
TEST_F(OverflowMenuMediatorTest,
TestPromotedDestinationsWhenNoHistoryUsageRanking) {
CreateMediator(/*is_incognito=*/NO);
// Show the new label badge for What's New.
ON_CALL(tracker_, ShouldTriggerHelpUI(testing::Ref(
feature_engagement::kIPHWhatsNewUpdatedFeature)))
.WillByDefault(testing::Return(true));
mediator_.engagementTracker = &tracker_;
syncer::MockSyncService syncService;
ON_CALL(syncService, GetUserActionableError())
.WillByDefault(
Return(syncer::SyncService::UserActionableError::kNeedsPassphrase));
mediator_.syncService = &syncService;
mediator_.model = model_;
// Verify the destinations to be promoted are put in the right rank and have
// the right badge.
EXPECT_NSEQ(kToolsMenuSettingsId,
mediator_.model.destinations[kNewDestinationsInsertionIndex]
.accessibilityIdentifier);
EXPECT_EQ(BadgeTypeError,
mediator_.model.destinations[kNewDestinationsInsertionIndex].badge);
EXPECT_NSEQ(kToolsMenuWhatsNewId,
mediator_.model.destinations[kNewDestinationsInsertionIndex + 1]
.accessibilityIdentifier);
EXPECT_EQ(
BadgeTypeNew,
mediator_.model.destinations[kNewDestinationsInsertionIndex + 1].badge);
EXPECT_EQ(8U, [mediator_.model.destinations count]);
}
// Tests that the actions have the correct longpress items set.
TEST_F(OverflowMenuMediatorTest, ActionLongpressItems) {
base::test::ScopedFeatureList scoped_feature_list(kOverflowMenuCustomization);
CreateMediator(/*is_incognito=*/NO);
mediator_.model = model_;
// The 1st action group should contain modifiable actions with 2 longpress
// items. All other action groups should contain un-modifiable actions with no
// longpress items.
for (NSUInteger index = 0; index < mediator_.model.actionGroups.count;
index++) {
OverflowMenuActionGroup* actionGroup = mediator_.model.actionGroups[index];
for (OverflowMenuAction* action in actionGroup.actions) {
if (index == 1) {
EXPECT_EQ(action.longPressItems.count, 2ul);
} else {
EXPECT_EQ(action.longPressItems.count, 0ul);
}
}
}
}
// Tests that the destinations have the correct longpress items set.
TEST_F(OverflowMenuMediatorTest, DestinationLongpressItems) {
base::test::ScopedFeatureList scoped_feature_list(kOverflowMenuCustomization);
CreateMediator(/*is_incognito=*/NO);
mediator_.model = model_;
// The 1st action group should contain modifiable actions with 2 longpress
// items. Settings and Site Info are exceptions, as they are not hideable, so
// they shold only have one longpress item
for (OverflowMenuDestination* destination in mediator_.model.destinations) {
overflow_menu::Destination destinationType =
static_cast<overflow_menu::Destination>(destination.destination);
if (destinationType == overflow_menu::Destination::Settings ||
destinationType == overflow_menu::Destination::SiteInfo) {
EXPECT_EQ(destination.longPressItems.count, 1ul);
continue;
}
EXPECT_EQ(destination.longPressItems.count, 2ul);
}
}
// Tests that when a destination becomes hidden during customization, the
// corresponding action gains a subtitle and a highlight.
TEST_F(OverflowMenuMediatorTest, DestinationHideShowsActionSubtitle) {
base::test::ScopedFeatureList scoped_feature_list(kOverflowMenuCustomization);
CreateMediator(/*is_incognito=*/NO);
mediator_.model = model_;
// Start customization sessions.
DestinationCustomizationModel* destinationModel =
orderer_.destinationCustomizationModel;
ActionCustomizationModel* actionModel = orderer_.actionCustomizationModel;
// Find destination in customization model.
OverflowMenuDestination* bookmarksDestination;
for (OverflowMenuDestination* destination in destinationModel
.shownDestinations) {
if (destination.accessibilityIdentifier == kToolsMenuBookmarksId) {
bookmarksDestination = destination;
break;
}
}
bookmarksDestination.shown = NO;
// Find corresponding action.
OverflowMenuAction* addToBookmarksAction;
for (OverflowMenuAction* action in actionModel.shownActions) {
if (action.accessibilityIdentifier == kToolsMenuAddToBookmarks) {
addToBookmarksAction = action;
break;
}
}
// Make sure that the corresponding action has a subtitle.
EXPECT_NE(addToBookmarksAction, nil);
EXPECT_TRUE(addToBookmarksAction.highlighted);
EXPECT_NE(addToBookmarksAction.subtitle, nil);
}
// Tests that when the the right metric is recorder when the Password Manager
// item is tapped.
TEST_F(OverflowMenuMediatorTest, OpenPasswordsMetricLogged) {
CreateMediator(/*is_incognito=*/NO);
mediator_.model = model_;
// Find the Password Manager destination.
OverflowMenuDestination* passwordsDestination;
for (OverflowMenuDestination* destination in mediator_.model.destinations) {
if (destination.accessibilityIdentifier == kToolsMenuPasswordsId) {
passwordsDestination = destination;
break;
}
}
EXPECT_NSNE(nil, passwordsDestination);
base::HistogramTester histogram_tester;
// Verify that bucker count is zero.
histogram_tester.ExpectBucketCount(
"PasswordManager.ManagePasswordsReferrer",
password_manager::ManagePasswordsReferrer::kChromeMenuItem, 0);
// Call Password Manager destination's handler.
passwordsDestination.handler();
// Bucket count should now be one.
histogram_tester.ExpectBucketCount(
"PasswordManager.ManagePasswordsReferrer",
password_manager::ManagePasswordsReferrer::kChromeMenuItem, 1);
}