// 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/ui/toolbar/adaptive_toolbar_mediator.h"
#import <memory>
#import "base/apple/foundation_util.h"
#import "base/files/scoped_temp_dir.h"
#import "base/memory/raw_ptr.h"
#import "base/strings/sys_string_conversions.h"
#import "base/strings/utf_string_conversions.h"
#import "components/open_from_clipboard/clipboard_recent_content.h"
#import "components/open_from_clipboard/fake_clipboard_recent_content.h"
#import "ios/chrome/browser/policy/model/policy_util.h"
#import "ios/chrome/browser/search_engines/model/template_url_service_factory.h"
#import "ios/chrome/browser/shared/model/browser/test/test_browser.h"
#import "ios/chrome/browser/shared/model/profile/test/test_profile_ios.h"
#import "ios/chrome/browser/shared/model/url/chrome_url_constants.h"
#import "ios/chrome/browser/shared/model/web_state_list/test/fake_web_state_list_delegate.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/commands/application_commands.h"
#import "ios/chrome/browser/shared/public/commands/browser_coordinator_commands.h"
#import "ios/chrome/browser/shared/public/commands/command_dispatcher.h"
#import "ios/chrome/browser/shared/public/commands/load_query_commands.h"
#import "ios/chrome/browser/shared/public/commands/qr_scanner_commands.h"
#import "ios/chrome/browser/shared/public/commands/settings_commands.h"
#import "ios/chrome/browser/ui/menu/browser_action_factory.h"
#import "ios/chrome/browser/ui/toolbar/test/toolbar_test_navigation_manager.h"
#import "ios/chrome/browser/ui/toolbar/toolbar_consumer.h"
#import "ios/chrome/browser/web/model/web_navigation_browser_agent.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/test/providers/voice_search/test_voice_search.h"
#import "ios/public/provider/chrome/browser/voice_search/voice_search_api.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_state.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 "third_party/ocmock/OCMock/OCMock.h"
#import "third_party/ocmock/gtest_support.h"
#import "ui/base/l10n/l10n_util.h"
@interface TestAdaptiveToolbarMediator
: AdaptiveToolbarMediator <CRWWebStateObserver, WebStateListObserving>
@end
@implementation TestAdaptiveToolbarMediator
@end
namespace {
MenuScenarioHistogram kTestMenuScenario = kMenuScenarioHistogramHistoryEntry;
static const int kNumberOfWebStates = 3;
static const char kTestUrl[] = "http://www.chromium.org";
class AdaptiveToolbarMediatorTest : public PlatformTest {
public:
AdaptiveToolbarMediatorTest() {
ios::provider::test::SetVoiceSearchEnabled(false);
TestChromeBrowserState::Builder test_cbs_builder;
test_cbs_builder.AddTestingFactory(
ios::TemplateURLServiceFactory::GetInstance(),
ios::TemplateURLServiceFactory::GetDefaultFactory());
chrome_browser_state_ = std::move(test_cbs_builder).Build();
test_browser_ = std::make_unique<TestBrowser>(chrome_browser_state_.get());
WebNavigationBrowserAgent::CreateForBrowser(test_browser_.get());
std::unique_ptr<ToolbarTestNavigationManager> navigation_manager =
std::make_unique<ToolbarTestNavigationManager>();
navigation_manager_ = navigation_manager.get();
test_web_state_ = std::make_unique<web::FakeWebState>();
test_web_state_->SetBrowserState(chrome_browser_state_.get());
test_web_state_->SetNavigationManager(std::move(navigation_manager));
test_web_state_->SetLoading(true);
web_state_ = test_web_state_.get();
mediator_ = [[TestAdaptiveToolbarMediator alloc] init];
mediator_.navigationBrowserAgent =
WebNavigationBrowserAgent::FromBrowser(test_browser_.get());
mediator_.actionFactory =
[[BrowserActionFactory alloc] initWithBrowser:test_browser_.get()
scenario:kTestMenuScenario];
mediator_.templateURLService =
ios::TemplateURLServiceFactory::GetForBrowserState(
chrome_browser_state_.get());
consumer_ = OCMProtocolMock(@protocol(ToolbarConsumer));
strict_consumer_ = OCMStrictProtocolMock(@protocol(ToolbarConsumer));
SetUpWebStateList();
mock_application_commands_handler_ =
OCMStrictProtocolMock(@protocol(ApplicationCommands));
[test_browser_->GetCommandDispatcher()
startDispatchingToTarget:mock_application_commands_handler_
forProtocol:@protocol(ApplicationCommands)];
mock_settings_commands_handler_ =
OCMStrictProtocolMock(@protocol(SettingsCommands));
[test_browser_->GetCommandDispatcher()
startDispatchingToTarget:mock_settings_commands_handler_
forProtocol:@protocol(SettingsCommands)];
mock_browser_coordinator_commands_handler_ =
OCMStrictProtocolMock(@protocol(BrowserCoordinatorCommands));
[test_browser_->GetCommandDispatcher()
startDispatchingToTarget:mock_browser_coordinator_commands_handler_
forProtocol:@protocol(BrowserCoordinatorCommands)];
mock_qr_scanner_commands_handler_ =
OCMStrictProtocolMock(@protocol(QRScannerCommands));
[test_browser_->GetCommandDispatcher()
startDispatchingToTarget:mock_qr_scanner_commands_handler_
forProtocol:@protocol(QRScannerCommands)];
mock_load_query_commands_handler_ =
OCMStrictProtocolMock(@protocol(LoadQueryCommands));
[test_browser_->GetCommandDispatcher()
startDispatchingToTarget:mock_load_query_commands_handler_
forProtocol:@protocol(LoadQueryCommands)];
[[UIPasteboard generalPasteboard] setItems:@[]];
ClipboardRecentContent::SetInstance(
std::make_unique<FakeClipboardRecentContent>());
}
// Explicitly disconnect the mediator so there won't be any WebStateList
// observers when web_state_list_ gets dealloc.
~AdaptiveToolbarMediatorTest() override {
ios::provider::test::SetVoiceSearchEnabled(false);
ClipboardRecentContent::SetInstance(nullptr);
[mediator_ disconnect];
}
protected:
web::WebTaskEnvironment task_environment_;
void SetUpWebStateList() {
web_state_list_ = std::make_unique<WebStateList>(&web_state_list_delegate_);
web_state_list_->InsertWebState(std::move(test_web_state_),
WebStateList::InsertionParams::AtIndex(0));
for (int i = 1; i < kNumberOfWebStates; i++) {
InsertNewWebState(i);
}
}
void InsertNewWebState(int index) {
auto web_state = std::make_unique<web::FakeWebState>();
web_state->SetBrowserState(chrome_browser_state_.get());
web_state->SetNavigationManager(
std::make_unique<web::FakeNavigationManager>());
GURL url("http://test/" + base::NumberToString(index));
web_state->SetCurrentURL(url);
web_state_list_->InsertWebState(
std::move(web_state), WebStateList::InsertionParams::AtIndex(index));
}
void SetUpActiveWebState() { web_state_list_->ActivateWebStateAt(0); }
TestAdaptiveToolbarMediator* mediator_;
std::unique_ptr<TestBrowser> test_browser_;
raw_ptr<web::FakeWebState> web_state_;
raw_ptr<ToolbarTestNavigationManager> navigation_manager_;
std::unique_ptr<WebStateList> web_state_list_;
FakeWebStateListDelegate web_state_list_delegate_;
id consumer_;
id strict_consumer_;
id mock_application_commands_handler_;
id mock_settings_commands_handler_;
id mock_browser_coordinator_commands_handler_;
id mock_qr_scanner_commands_handler_;
id mock_load_query_commands_handler_;
std::unique_ptr<TestChromeBrowserState> chrome_browser_state_;
private:
std::unique_ptr<web::FakeWebState> test_web_state_;
};
// Test no setup is being done on the Toolbar if there's no Webstate.
TEST_F(AdaptiveToolbarMediatorTest, TestToolbarSetupWithNoWebstate) {
mediator_.consumer = consumer_;
[[consumer_ reject] setCanGoForward:NO];
[[consumer_ reject] setCanGoBack:NO];
[[consumer_ reject] setLoadingState:YES];
}
// Test no setup is being done on the Toolbar if there's no active Webstate.
TEST_F(AdaptiveToolbarMediatorTest, TestToolbarSetupWithNoActiveWebstate) {
mediator_.webStateList = web_state_list_.get();
mediator_.consumer = consumer_;
[[consumer_ reject] setCanGoForward:NO];
[[consumer_ reject] setCanGoBack:NO];
[[consumer_ reject] setLoadingState:YES];
}
// Test no WebstateList related setup is being done on the Toolbar if there's no
// WebstateList.
TEST_F(AdaptiveToolbarMediatorTest, TestToolbarSetupWithNoWebstateList) {
mediator_.consumer = consumer_;
[[[consumer_ reject] ignoringNonObjectArgs] setTabCount:0
addedInBackground:NO];
}
// Tests the Toolbar Setup gets called when the mediator's WebState and Consumer
// have been set.
TEST_F(AdaptiveToolbarMediatorTest, TestToolbarSetup) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
[[consumer_ verify] setCanGoForward:NO];
[[consumer_ verify] setCanGoBack:NO];
[[consumer_ verify] setLoadingState:YES];
[[consumer_ verify] setShareMenuEnabled:NO];
}
// Tests the Toolbar Setup gets called when the mediator's WebState and Consumer
// have been set in reverse order.
TEST_F(AdaptiveToolbarMediatorTest, TestToolbarSetupReverse) {
mediator_.consumer = consumer_;
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
[[consumer_ verify] setCanGoForward:NO];
[[consumer_ verify] setCanGoBack:NO];
[[consumer_ verify] setLoadingState:YES];
[[consumer_ verify] setShareMenuEnabled:NO];
}
// Test the WebstateList related setup gets called when the mediator's WebState
// and Consumer have been set.
TEST_F(AdaptiveToolbarMediatorTest, TestWebstateListRelatedSetup) {
mediator_.webStateList = web_state_list_.get();
mediator_.consumer = consumer_;
[[consumer_ verify] setTabCount:3 addedInBackground:NO];
}
// Test the WebstateList related setup gets called when the mediator's WebState
// and Consumer have been set in reverse order.
TEST_F(AdaptiveToolbarMediatorTest, TestWebstateListRelatedSetupReverse) {
mediator_.consumer = consumer_;
mediator_.webStateList = web_state_list_.get();
[[consumer_ verify] setTabCount:3 addedInBackground:NO];
}
// Tests the Toolbar is updated when the Webstate observer method
// DidStartLoading is triggered by SetLoading.
TEST_F(AdaptiveToolbarMediatorTest, TestDidStartLoading) {
// Change the default loading state to false to verify the Webstate
// callback with true.
web_state_->SetLoading(false);
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
web_state_->SetLoading(true);
[[consumer_ verify] setLoadingState:YES];
}
// Tests the Toolbar is updated when the Webstate observer method DidStopLoading
// is triggered by SetLoading.
TEST_F(AdaptiveToolbarMediatorTest, TestDidStopLoading) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
web_state_->SetLoading(false);
[[consumer_ verify] setLoadingState:NO];
}
// Tests the Toolbar is not updated when the Webstate observer method
// DidStartLoading is triggered by SetLoading on the NTP.
TEST_F(AdaptiveToolbarMediatorTest, TestDidStartLoadingNTP) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
web_state_->SetLoading(false);
web_state_->SetVisibleURL(GURL(kChromeUINewTabURL));
web_state_->SetLoading(true);
[[consumer_ verify] setLoadingState:NO];
}
// Tests the Toolbar is updated when the Webstate observer method
// DidLoadPageWithSuccess is triggered by OnPageLoaded.
TEST_F(AdaptiveToolbarMediatorTest, TestDidLoadPageWithSucess) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
navigation_manager_->set_can_go_forward(true);
navigation_manager_->set_can_go_back(true);
web_state_->SetCurrentURL(GURL(kTestUrl));
web_state_->OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
[[consumer_ verify] setCanGoForward:YES];
[[consumer_ verify] setCanGoBack:YES];
[[consumer_ verify] setShareMenuEnabled:YES];
}
// Tests the Toolbar is updated when the Webstate observer method
// didFinishNavigation is called.
TEST_F(AdaptiveToolbarMediatorTest, TestDidFinishNavigation) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
navigation_manager_->set_can_go_forward(true);
navigation_manager_->set_can_go_back(true);
web_state_->SetCurrentURL(GURL(kTestUrl));
web::FakeNavigationContext context;
web_state_->OnNavigationFinished(&context);
[[consumer_ verify] setCanGoForward:YES];
[[consumer_ verify] setCanGoBack:YES];
[[consumer_ verify] setShareMenuEnabled:YES];
}
// Tests the Toolbar is updated when the Webstate observer method
// didChangeVisibleSecurityState is called.
TEST_F(AdaptiveToolbarMediatorTest, TestDidChangeVisibleSecurityState) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
navigation_manager_->set_can_go_forward(true);
navigation_manager_->set_can_go_back(true);
web_state_->SetCurrentURL(GURL(kTestUrl));
web_state_->OnVisibleSecurityStateChanged();
[[consumer_ verify] setCanGoForward:YES];
[[consumer_ verify] setCanGoBack:YES];
[[consumer_ verify] setShareMenuEnabled:YES];
}
// Tests the Toolbar is updated when the Webstate observer method
// didChangeLoadingProgress is called.
TEST_F(AdaptiveToolbarMediatorTest, TestLoadingProgress) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
[mediator_ webState:web_state_ didChangeLoadingProgress:0.42];
[[consumer_ verify] setLoadingProgressFraction:0.42];
}
// Tests the Toolbar is updated when Webstate observer method
// didChangeBackForwardState is called.
TEST_F(AdaptiveToolbarMediatorTest, TestDidChangeBackForwardState) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
navigation_manager_->set_can_go_forward(true);
navigation_manager_->set_can_go_back(true);
web_state_->OnBackForwardStateChanged();
[[consumer_ verify] setCanGoForward:YES];
[[consumer_ verify] setCanGoBack:YES];
}
// Test that increasing the number of Webstates will update the consumer with
// the right value.
TEST_F(AdaptiveToolbarMediatorTest, TestIncreaseNumberOfWebstates) {
mediator_.webStateList = web_state_list_.get();
mediator_.consumer = consumer_;
InsertNewWebState(0);
[[consumer_ verify] setTabCount:kNumberOfWebStates + 1 addedInBackground:YES];
}
// Test that decreasing the number of Webstates will update the consumer with
// the right value.
TEST_F(AdaptiveToolbarMediatorTest, TestDecreaseNumberOfWebstates) {
mediator_.webStateList = web_state_list_.get();
mediator_.consumer = consumer_;
web_state_list_->DetachWebStateAt(0);
[[consumer_ verify] setTabCount:kNumberOfWebStates - 1 addedInBackground:NO];
}
// Test that consumer is informed that voice search is enabled.
TEST_F(AdaptiveToolbarMediatorTest, TestVoiceSearchProviderEnabled) {
ios::provider::test::SetVoiceSearchEnabled(true);
OCMExpect([consumer_ setVoiceSearchEnabled:YES]);
mediator_.consumer = consumer_;
EXPECT_OCMOCK_VERIFY(consumer_);
}
// Test that consumer is informed that voice search is not enabled.
TEST_F(AdaptiveToolbarMediatorTest, TestVoiceSearchProviderNotEnabled) {
ios::provider::test::SetVoiceSearchEnabled(false);
OCMExpect([consumer_ setVoiceSearchEnabled:NO]);
mediator_.consumer = consumer_;
EXPECT_OCMOCK_VERIFY(consumer_);
}
// Test that updating the consumer for a specific webState works.
TEST_F(AdaptiveToolbarMediatorTest, TestUpdateConsumerForWebState) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
mediator_.consumer = consumer_;
auto navigation_manager = std::make_unique<ToolbarTestNavigationManager>();
navigation_manager->set_can_go_forward(true);
navigation_manager->set_can_go_back(true);
std::unique_ptr<web::FakeWebState> test_web_state =
std::make_unique<web::FakeWebState>();
test_web_state->SetNavigationManager(std::move(navigation_manager));
test_web_state->SetCurrentURL(GURL(kTestUrl));
test_web_state->OnPageLoaded(web::PageLoadCompletionStatus::SUCCESS);
OCMExpect([consumer_ setCanGoForward:YES]);
OCMExpect([consumer_ setCanGoBack:YES]);
OCMExpect([consumer_ setShareMenuEnabled:YES]);
[mediator_ updateConsumerForWebState:test_web_state.get()];
EXPECT_OCMOCK_VERIFY(consumer_);
}
// Tests the menu elements.
TEST_F(AdaptiveToolbarMediatorTest, MenuElements) {
mediator_.webStateList = web_state_list_.get();
SetUpActiveWebState();
UIMenu* new_tab_menu =
[mediator_ menuForButtonOfType:AdaptiveToolbarButtonTypeNewTab];
ASSERT_EQ(4U, new_tab_menu.children.count);
for (UIMenuElement* element in new_tab_menu.children) {
ASSERT_TRUE([element isKindOfClass:[UIAction class]]);
UIAction* action = (UIAction*)element;
EXPECT_EQ(0U, action.attributes);
}
UIMenu* tab_grid_menu =
[mediator_ menuForButtonOfType:AdaptiveToolbarButtonTypeTabGrid];
ASSERT_EQ(3U, tab_grid_menu.children.count);
ASSERT_TRUE([tab_grid_menu.children[0] isKindOfClass:[UIAction class]]);
UIAction* close_tab = (UIAction*)tab_grid_menu.children[0];
EXPECT_NSEQ(l10n_util::GetNSString(IDS_IOS_TOOLS_MENU_CLOSE_TAB),
close_tab.title);
EXPECT_EQ(UIMenuElementAttributesDestructive, close_tab.attributes);
ASSERT_TRUE([tab_grid_menu.children[1] isKindOfClass:[UIAction class]]);
UIAction* action = (UIAction*)tab_grid_menu.children[1];
EXPECT_EQ(0U, action.attributes);
ASSERT_TRUE([tab_grid_menu.children[2] isKindOfClass:[UIAction class]]);
action = (UIAction*)tab_grid_menu.children[2];
EXPECT_EQ(0U, action.attributes);
}
// Tests the back/forward items for the menu.
TEST_F(AdaptiveToolbarMediatorTest, MenuElementsBackForward) {
std::unique_ptr<web::FakeNavigationManager> navigation_manager =
std::make_unique<web::FakeNavigationManager>();
navigation_manager->AddItem(GURL("http://chromium.org/1"),
ui::PageTransition::PAGE_TRANSITION_LINK);
navigation_manager->AddItem(GURL("http://chromium.org/2"),
ui::PageTransition::PAGE_TRANSITION_LINK);
navigation_manager->AddItem(GURL("http://chromium.org/current"),
ui::PageTransition::PAGE_TRANSITION_LINK);
navigation_manager->AddItem(GURL("http://chromium.org/4"),
ui::PageTransition::PAGE_TRANSITION_LINK);
navigation_manager->AddItem(GURL("http://chromium.org/5"),
ui::PageTransition::PAGE_TRANSITION_LINK);
navigation_manager->AddItem(GURL("http://chromium.org/6"),
ui::PageTransition::PAGE_TRANSITION_LINK);
navigation_manager->GoBack();
navigation_manager->GoBack();
navigation_manager->GoBack();
auto web_state = std::make_unique<web::FakeWebState>();
web_state->SetBrowserState(chrome_browser_state_.get());
web_state->SetNavigationManager(std::move(navigation_manager));
web_state_list_->InsertWebState(
std::move(web_state),
WebStateList::InsertionParams::Automatic().Activate());
mediator_.webStateList = web_state_list_.get();
mediator_.consumer = consumer_;
UIMenu* back_menu =
[mediator_ menuForButtonOfType:AdaptiveToolbarButtonTypeBack];
ASSERT_EQ(2U, back_menu.children.count);
EXPECT_NSEQ(@"chromium.org/2", back_menu.children[0].title);
EXPECT_NSEQ(@"chromium.org/1", back_menu.children[1].title);
UIMenu* forward_menu =
[mediator_ menuForButtonOfType:AdaptiveToolbarButtonTypeForward];
ASSERT_EQ(3U, forward_menu.children.count);
EXPECT_NSEQ(@"chromium.org/4", forward_menu.children[0].title);
EXPECT_NSEQ(@"chromium.org/5", forward_menu.children[1].title);
EXPECT_NSEQ(@"chromium.org/6", forward_menu.children[2].title);
}
} // namespace