blob: 9e9275c684f4eccaf033265152afc96cb2d929c7 [file] [log] [blame]
// Copyright 2018 The Chromium 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 "ios/chrome/browser/ui/popup_menu/popup_menu_mediator.h"
#include "base/strings/sys_string_conversions.h"
#include "base/test/scoped_feature_list.h"
#include "base/time/default_clock.h"
#include "components/bookmarks/browser/bookmark_model.h"
#include "components/bookmarks/browser/bookmark_utils.h"
#include "components/bookmarks/common/bookmark_pref_names.h"
#include "components/bookmarks/test/bookmark_test_helpers.h"
#include "components/feature_engagement/test/mock_tracker.h"
#include "components/language/ios/browser/ios_language_detection_tab_helper.h"
#include "components/prefs/pref_registry_simple.h"
#include "components/prefs/testing_pref_service.h"
#include "components/reading_list/core/reading_list_model_impl.h"
#include "components/translate/core/browser/translate_prefs.h"
#include "ios/chrome/browser/bookmarks/bookmark_model_factory.h"
#import "ios/chrome/browser/browser_state/test_chrome_browser_state.h"
#import "ios/chrome/browser/main/test_browser.h"
#import "ios/chrome/browser/overlays/public/overlay_presenter.h"
#import "ios/chrome/browser/overlays/public/overlay_request.h"
#import "ios/chrome/browser/overlays/public/overlay_request_queue.h"
#import "ios/chrome/browser/overlays/public/web_content_area/java_script_dialog_overlay.h"
#include "ios/chrome/browser/overlays/test/fake_overlay_presentation_context.h"
#include "ios/chrome/browser/policy/policy_features.h"
#import "ios/chrome/browser/ui/popup_menu/cells/popup_menu_tools_item.h"
#import "ios/chrome/browser/ui/popup_menu/popup_menu_constants.h"
#import "ios/chrome/browser/ui/popup_menu/public/popup_menu_table_view_controller.h"
#import "ios/chrome/browser/ui/toolbar/test/toolbar_test_navigation_manager.h"
#import "ios/chrome/browser/ui/toolbar/test/toolbar_test_web_state.h"
#include "ios/chrome/browser/web/chrome_web_client.h"
#import "ios/chrome/browser/web/chrome_web_test.h"
#include "ios/chrome/browser/web/features.h"
#import "ios/chrome/browser/web/font_size_tab_helper.h"
#include "ios/chrome/browser/web_state_list/fake_web_state_list_delegate.h"
#include "ios/chrome/browser/web_state_list/web_state_list.h"
#import "ios/chrome/browser/web_state_list/web_state_list_observer_bridge.h"
#import "ios/chrome/browser/web_state_list/web_state_opener.h"
#include "ios/public/provider/chrome/browser/chrome_browser_provider.h"
#import "ios/public/provider/chrome/browser/user_feedback/user_feedback_provider.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/test_navigation_manager.h"
#import "ios/web/public/test/fakes/test_web_state.h"
#include "ios/web/public/test/web_task_environment.h"
#import "ios/web/public/web_state_observer_bridge.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#include "third_party/ocmock/gtest_support.h"
#include "ui/base/device_form_factor.h"
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
using bookmarks::BookmarkModel;
using java_script_dialog_overlays::JavaScriptDialogRequest;
@interface FakePopupMenuConsumer : NSObject <PopupMenuConsumer>
@property(nonatomic, strong)
NSArray<NSArray<TableViewItem<PopupMenuItem>*>*>* popupMenuItems;
@end
@implementation FakePopupMenuConsumer
@synthesize itemToHighlight;
- (void)itemsHaveChanged:(NSArray<TableViewItem<PopupMenuItem>*>*)items {
// Do nothing.
}
@end
namespace {
const int kNumberOfWebStates = 3;
} // namespace
@interface TestPopupMenuMediator
: PopupMenuMediator<CRWWebStateObserver, WebStateListObserving>
@end
@implementation TestPopupMenuMediator
@end
class PopupMenuMediatorTest : public ChromeWebTest {
public:
PopupMenuMediatorTest() : ChromeWebTest(std::make_unique<ChromeWebClient>()) {
reading_list_model_.reset(new ReadingListModelImpl(
nullptr, nullptr, base::DefaultClock::GetInstance()));
popup_menu_ = OCMClassMock([PopupMenuTableViewController class]);
popup_menu_strict_ =
OCMStrictClassMock([PopupMenuTableViewController class]);
OCMExpect([popup_menu_strict_ setPopupMenuItems:[OCMArg any]]);
OCMExpect([popup_menu_strict_ setDelegate:[OCMArg any]]);
SetUpWebStateList();
// Set up the TestBrowser.
TestChromeBrowserState::Builder browser_state_builder;
browser_state_ = browser_state_builder.Build();
browser_ = std::make_unique<TestBrowser>(browser_state_.get(),
web_state_list_.get());
// Set up the OverlayPresenter.
OverlayPresenter::FromBrowser(browser_.get(),
OverlayModality::kWebContentArea)
->SetPresentationContext(&presentation_context_);
}
// Explicitly disconnect the mediator so there won't be any WebStateList
// observers when web_state_list_ gets dealloc.
~PopupMenuMediatorTest() override {
[mediator_ disconnect];
}
protected:
PopupMenuMediator* CreateMediator(PopupMenuType type,
BOOL is_incognito,
BOOL trigger_incognito_hint) {
mediator_ =
[[PopupMenuMediator alloc] initWithType:type
isIncognito:is_incognito
readingListModel:reading_list_model_.get()
triggerNewIncognitoTabTip:trigger_incognito_hint
browserPolicyConnector:nil];
return mediator_;
}
void CreatePrefs() {
prefs_ = std::make_unique<TestingPrefServiceSimple>();
prefs_->registry()->RegisterBooleanPref(
bookmarks::prefs::kEditBookmarksEnabled,
/*default_value=*/true);
}
void SetUpBookmarks() {
browser_state_->CreateBookmarkModel(false);
bookmark_model_ =
ios::BookmarkModelFactory::GetForBrowserState(browser_state_.get());
bookmarks::test::WaitForBookmarkModelToLoad(bookmark_model_);
mediator_.bookmarkModel = bookmark_model_;
}
void SetUpWebStateList() {
auto navigation_manager = std::make_unique<ToolbarTestNavigationManager>();
navigation_manager_ = navigation_manager.get();
navigation_item_ = web::NavigationItem::Create();
navigation_item_->SetURL(GURL("http://chromium.org"));
navigation_manager->SetVisibleItem(navigation_item_.get());
std::unique_ptr<ToolbarTestWebState> test_web_state =
std::make_unique<ToolbarTestWebState>();
test_web_state->SetNavigationManager(std::move(navigation_manager));
test_web_state->SetLoading(true);
web_state_ = test_web_state.get();
web_state_list_ = std::make_unique<WebStateList>(&web_state_list_delegate_);
web_state_list_->InsertWebState(0, std::move(test_web_state),
WebStateList::INSERT_FORCE_INDEX,
WebStateOpener());
for (int i = 1; i < kNumberOfWebStates; i++) {
InsertNewWebState(i);
}
}
void InsertNewWebState(int index) {
auto web_state = std::make_unique<web::TestWebState>();
GURL url("http://test/" + std::to_string(index));
web_state->SetCurrentURL(url);
web_state_list_->InsertWebState(index, std::move(web_state),
WebStateList::INSERT_FORCE_INDEX,
WebStateOpener());
}
void SetUpActiveWebState() {
// PopupMenuMediator expects an language::IOSLanguageDetectionTabHelper for
// the currently active WebState.
language::IOSLanguageDetectionTabHelper::CreateForWebState(
web_state_list_->GetWebStateAt(0), /*url_language_histogram=*/nullptr);
web_state_list_->ActivateWebStateAt(0);
}
// Checks that the popup_menu_ is receiving a number of items corresponding to
// |number_items|.
void CheckMediatorSetItems(NSArray<NSNumber*>* number_items) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
auto same_number_items = ^BOOL(id items) {
if (![items isKindOfClass:[NSArray class]])
return NO;
if ([items count] != number_items.count)
return NO;
for (NSUInteger index = 0; index < number_items.count; index++) {
NSArray* section = [items objectAtIndex:index];
if (section.count != number_items[index].unsignedIntegerValue)
return NO;
}
return YES;
};
OCMExpect([popup_menu_
setPopupMenuItems:[OCMArg checkWithBlock:same_number_items]]);
mediator_.popupMenu = popup_menu_;
EXPECT_OCMOCK_VERIFY(popup_menu_);
}
bool HasItem(FakePopupMenuConsumer* consumer,
NSString* accessibility_identifier,
BOOL enabled) {
for (NSArray* innerArray in consumer.popupMenuItems) {
for (PopupMenuToolsItem* item in innerArray) {
if (item.accessibilityIdentifier == accessibility_identifier)
return item.enabled == enabled;
}
}
return NO;
}
FakeOverlayPresentationContext presentation_context_;
std::unique_ptr<WebStateList> web_state_list_;
FakeWebStateListDelegate web_state_list_delegate_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
std::unique_ptr<Browser> browser_;
PopupMenuMediator* mediator_;
BookmarkModel* bookmark_model_;
std::unique_ptr<TestingPrefServiceSimple> prefs_;
std::unique_ptr<ReadingListModelImpl> reading_list_model_;
ToolbarTestWebState* web_state_;
ToolbarTestNavigationManager* navigation_manager_;
std::unique_ptr<web::NavigationItem> navigation_item_;
id popup_menu_;
// Mock refusing all calls except -setPopupMenuItems:.
id popup_menu_strict_;
};
// Tests that the feature engagement tracker get notified when the mediator is
// disconnected and the tracker wants the notification badge displayed.
TEST_F(PopupMenuMediatorTest, TestFeatureEngagementDisconnect) {
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
feature_engagement::test::MockTracker tracker;
EXPECT_CALL(tracker, ShouldTriggerHelpUI(testing::_))
.WillRepeatedly(testing::Return(true));
mediator_.popupMenu = popup_menu_;
mediator_.engagementTracker = &tracker;
// There may be one or more Tools Menu items that use engagement trackers.
EXPECT_CALL(tracker, Dismissed(testing::_)).Times(testing::AtLeast(1));
[mediator_ disconnect];
}
// Tests that the mediator is returning the right number of items and sections
// for the Tools Menu type.
TEST_F(PopupMenuMediatorTest, TestToolsMenuItemsCount) {
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
NSUInteger number_of_action_items = 7;
if (ios::GetChromeBrowserProvider()
->GetUserFeedbackProvider()
->IsUserFeedbackEnabled()) {
number_of_action_items++;
}
// Text zoom is currently disabled on iPad. See crbug.com/1061119.
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
number_of_action_items++;
}
// Checks that Tools Menu has the right number of items in each section.
CheckMediatorSetItems(@[
// Stop/Reload, New Tab, New Incognito Tab
@(3),
// 4 collections + Settings
@(5),
// Other actions, depending on configuration
@(number_of_action_items)
]);
}
// Tests that the mediator is returning the right number of items and sections
// for the Tab Grid type, in non-incognito.
TEST_F(PopupMenuMediatorTest, TestTabGridMenuNonIncognito) {
CreateMediator(PopupMenuTypeTabGrid, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
CheckMediatorSetItems(@[
// New Tab, New Incognito Tab
@(2),
// Close Tab
@(1)
]);
}
// Tests that the mediator is returning the right number of items and sections
// for the Tab Grid type, in incognito.
TEST_F(PopupMenuMediatorTest, TestTabGridMenuIncognito) {
CreateMediator(PopupMenuTypeTabGrid, /*is_incognito=*/YES,
/*trigger_incognito_hint=*/NO);
CheckMediatorSetItems(@[
// New Tab, New Incognito Tab
@(2),
// Close Tab
@(1)
]);
}
// Tests that the mediator is asking for an item to be highlighted when asked.
TEST_F(PopupMenuMediatorTest, TestNewIncognitoHint) {
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/YES);
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
OCMExpect([popup_menu_ setItemToHighlight:[OCMArg isNotNil]]);
mediator_.popupMenu = popup_menu_;
EXPECT_OCMOCK_VERIFY(popup_menu_);
}
// Test that the mediator isn't asking for an highlighted item.
TEST_F(PopupMenuMediatorTest, TestNewIncognitoNoHint) {
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
[[popup_menu_ reject] setItemToHighlight:[OCMArg any]];
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.popupMenu = popup_menu_;
}
// Tests that the mediator is asking for an item to be highlighted when asked.
TEST_F(PopupMenuMediatorTest, TestNewIncognitoHintTabGrid) {
CreateMediator(PopupMenuTypeTabGrid, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/YES);
OCMExpect([popup_menu_ setItemToHighlight:[OCMArg isNotNil]]);
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.popupMenu = popup_menu_;
EXPECT_OCMOCK_VERIFY(popup_menu_);
}
// Tests that the items returned by the mediator are correctly enabled on a
// WebPage.
TEST_F(PopupMenuMediatorTest, TestItemsStatusOnWebPage) {
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
mediator_.webStateList = web_state_list_.get();
FakePopupMenuConsumer* consumer = [[FakePopupMenuConsumer alloc] init];
mediator_.popupMenu = consumer;
SetUpActiveWebState();
web::FakeNavigationContext context;
web_state_->OnNavigationFinished(&context);
EXPECT_TRUE(HasItem(consumer, kToolsMenuNewTabId, /*enabled=*/YES));
EXPECT_TRUE(HasItem(consumer, kToolsMenuSiteInformation, /*enabled=*/YES));
}
// Tests that the items returned by the mediator are correctly enabled on the
// NTP.
TEST_F(PopupMenuMediatorTest, TestItemsStatusOnNTP) {
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
mediator_.webStateList = web_state_list_.get();
FakePopupMenuConsumer* consumer = [[FakePopupMenuConsumer alloc] init];
mediator_.popupMenu = consumer;
SetUpActiveWebState();
navigation_item_->SetVirtualURL(GURL("chrome://newtab"));
web::FakeNavigationContext context;
web_state_->OnNavigationFinished(&context);
EXPECT_TRUE(HasItem(consumer, kToolsMenuNewTabId, /*enabled=*/YES));
EXPECT_TRUE(HasItem(consumer, kToolsMenuSiteInformation, /*enabled=*/YES));
}
// Tests that the "Read Later" button is disabled while overlay UI is displayed
// in OverlayModality::kWebContentArea.
TEST_F(PopupMenuMediatorTest, TestReadLaterDisabled) {
const GURL kUrl("https://chromium.test");
web_state_->SetCurrentURL(kUrl);
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
mediator_.webStateList = web_state_list_.get();
mediator_.webContentAreaOverlayPresenter = OverlayPresenter::FromBrowser(
browser_.get(), OverlayModality::kWebContentArea);
FakePopupMenuConsumer* consumer = [[FakePopupMenuConsumer alloc] init];
mediator_.popupMenu = consumer;
SetUpActiveWebState();
ASSERT_TRUE(HasItem(consumer, 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<JavaScriptDialogRequest>(
web::JAVASCRIPT_DIALOG_TYPE_ALERT, web_state_, kUrl,
/*is_main_frame=*/true, @"message",
/*default_text_field_value=*/nil));
EXPECT_TRUE(HasItem(consumer, kToolsMenuReadLater, /*enabled=*/NO));
// Cancel the request and verify that the "Read Later" button is enabled.
queue->CancelAllRequests();
EXPECT_TRUE(HasItem(consumer, kToolsMenuReadLater, /*enabled=*/YES));
}
// Tests that the "Text Zoom..." button is disabled on non-HTML pages.
TEST_F(PopupMenuMediatorTest, TestTextZoomDisabled) {
// This feature is currently disabled on iPad. See crbug.com/1061119.
if (ui::GetDeviceFormFactor() == ui::DEVICE_FORM_FACTOR_TABLET) {
return;
}
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(web::kWebPageTextAccessibility);
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
mediator_.webStateList = web_state_list_.get();
FakePopupMenuConsumer* consumer = [[FakePopupMenuConsumer alloc] init];
mediator_.popupMenu = consumer;
FontSizeTabHelper::CreateForWebState(web_state_list_->GetWebStateAt(0));
SetUpActiveWebState();
EXPECT_TRUE(HasItem(consumer, 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(consumer, kToolsMenuTextZoom, /*enabled=*/NO));
}
// Tests that this feature is disabled on iPad, no matter the state of the
// Feature flag. See crbug.com/1061119.
TEST_F(PopupMenuMediatorTest, TestTextZoomDisabledIPad) {
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
return;
}
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(web::kWebPageTextAccessibility);
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
mediator_.webStateList = web_state_list_.get();
FakePopupMenuConsumer* consumer = [[FakePopupMenuConsumer alloc] init];
mediator_.popupMenu = consumer;
FontSizeTabHelper::CreateForWebState(web_state_list_->GetWebStateAt(0));
SetUpActiveWebState();
EXPECT_FALSE(HasItem(consumer, kToolsMenuTextZoom, /*enabled=*/YES));
}
// 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(PopupMenuMediatorTest, TestBookmarksToolsMenuButtons) {
const GURL url("https://bookmarked.url");
web_state_->SetCurrentURL(url);
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
SetUpBookmarks();
bookmarks::AddIfNotBookmarked(bookmark_model_, url,
base::SysNSStringToUTF16(@"Test bookmark"));
mediator_.webStateList = web_state_list_.get();
FakePopupMenuConsumer* consumer = [[FakePopupMenuConsumer alloc] init];
mediator_.popupMenu = consumer;
EXPECT_TRUE(HasItem(consumer, kToolsMenuAddToBookmarks, /*enabled=*/YES));
SetUpActiveWebState();
EXPECT_FALSE(HasItem(consumer, kToolsMenuAddToBookmarks, /*enabled=*/YES));
EXPECT_TRUE(HasItem(consumer, kToolsMenuEditBookmark, /*enabled=*/YES));
bookmark_model_->RemoveAllUserBookmarks();
EXPECT_TRUE(HasItem(consumer, kToolsMenuAddToBookmarks, /*enabled=*/YES));
EXPECT_FALSE(HasItem(consumer, kToolsMenuEditBookmark, /*enabled=*/YES));
}
// Tests that the bookmark button is disabled when EditBookmarksEnabled pref is
// changed to false.
TEST_F(PopupMenuMediatorTest, TestDisableBookmarksButton) {
base::test::ScopedFeatureList feature_list;
feature_list.InitAndEnableFeature(kEditBookmarksIOS);
CreateMediator(PopupMenuTypeToolsMenu, /*is_incognito=*/NO,
/*trigger_incognito_hint=*/NO);
CreatePrefs();
FakePopupMenuConsumer* consumer = [[FakePopupMenuConsumer alloc] init];
mediator_.popupMenu = consumer;
mediator_.prefService = prefs_.get();
EXPECT_TRUE(HasItem(consumer, kToolsMenuAddToBookmarks, /*enabled=*/YES));
prefs_->SetBoolean(bookmarks::prefs::kEditBookmarksEnabled, false);
EXPECT_TRUE(HasItem(consumer, kToolsMenuAddToBookmarks, /*enabled=*/NO));
}