// 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/tab_switcher/tab_grid/pinned_tabs/pinned_tabs_mediator.h"
#import <UIKit/UIKit.h>
#import "base/memory/raw_ptr.h"
#import "base/test/ios/wait_util.h"
#import "base/test/metrics/histogram_tester.h"
#import "base/test/scoped_feature_list.h"
#import "ios/chrome/browser/drag_and_drop/model/drag_item_util.h"
#import "ios/chrome/browser/shared/model/browser/browser_list.h"
#import "ios/chrome/browser/shared/model/browser/browser_list_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/web_state_list/test/web_state_list_builder_from_description.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_opener.h"
#import "ios/chrome/browser/shared/public/features/features.h"
#import "ios/chrome/browser/ui/tab_switcher/tab_collection_drag_drop_metrics.h"
#import "ios/chrome/browser/ui/tab_switcher/test/fake_drag_session.h"
#import "ios/chrome/browser/ui/tab_switcher/test/fake_drop_session.h"
#import "ios/chrome/browser/ui/tab_switcher/test/fake_pinned_tab_collection_consumer.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 "testing/platform_test.h"
#import "third_party/ocmock/OCMock/OCMock.h"
#import "ui/base/device_form_factor.h"
namespace {
// Returns a GURL for the given `index`.
GURL GURLWithIndex(int index) {
return GURL("http://test/url" + base::NumberToString(index));
}
// Returns a FakeDropSession for the given `web_state`.
FakeDropSession* FakeDropSessionWithWebState(web::WebState* web_state) {
UIDragItem* drag_item = CreateTabDragItem(web_state);
FakeDropSession* drop_session =
[[FakeDropSession alloc] initWithItems:@[ drag_item ]];
return drop_session;
}
} // namespace
class PinnedTabsMediatorTest : public PlatformTest {
public:
PinnedTabsMediatorTest() {
TestChromeBrowserState::Builder builder;
browser_state_ = std::move(builder).Build();
regular_browser_ = std::make_unique<TestBrowser>(browser_state_.get());
incognito_browser_ = std::make_unique<TestBrowser>(
browser_state_->GetOffTheRecordChromeBrowserState());
browser_list_ =
BrowserListFactory::GetForBrowserState(browser_state_.get());
browser_list_->AddBrowser(regular_browser_.get());
browser_list_->AddBrowser(incognito_browser_.get());
// The Pinned Tabs feature is not available on iPad.
if (ui::GetDeviceFormFactor() != ui::DEVICE_FORM_FACTOR_TABLET) {
consumer_ = [[FakePinnedTabCollectionConsumer alloc] init];
mediator_ = [[PinnedTabsMediator alloc] initWithConsumer:consumer_];
mediator_.browser = regular_browser_.get();
}
}
// Creates a FakeWebState with a navigation history containing exactly only
// the given `url`.
std::unique_ptr<web::FakeWebState> CreateFakeWebStateWithURL(
const GURL& url) {
std::unique_ptr<web::FakeWebState> web_state =
std::make_unique<web::FakeWebState>();
auto navigation_manager = std::make_unique<web::FakeNavigationManager>();
navigation_manager->AddItem(url, ui::PAGE_TRANSITION_LINK);
navigation_manager->SetLastCommittedItem(
navigation_manager->GetItemAtIndex(0));
web_state->SetNavigationManager(std::move(navigation_manager));
web_state->SetBrowserState(browser_state_.get());
web_state->SetNavigationItemCount(1);
web_state->SetCurrentURL(url);
return web_state;
}
// Checks that the drag item origin metric is logged in UMA.
void ExpectThatDragItemOriginMetricLogged(DragItemOrigin origin,
int count = 1) {
histogram_tester_.ExpectUniqueSample(kUmaPinnedViewDragOrigin, origin,
count);
}
protected:
std::unique_ptr<TestBrowser> regular_browser_;
std::unique_ptr<Browser> incognito_browser_;
FakePinnedTabCollectionConsumer* consumer_;
PinnedTabsMediator* mediator_;
base::HistogramTester histogram_tester_;
private:
web::WebTaskEnvironment task_environment_;
base::test::ScopedFeatureList feature_list_;
raw_ptr<BrowserList> browser_list_;
std::unique_ptr<TestChromeBrowserState> browser_state_;
};
// Tests that the consumer is notified when a web state is pinned.
TEST_F(PinnedTabsMediatorTest, ConsumerInsertItem) {
// The Pinned Tabs feature is not available on iPad.
if (!IsPinnedTabsEnabled()) {
return;
}
// Inserts two new pinned tabs.
std::unique_ptr<web::WebState> web_state1 =
CreateFakeWebStateWithURL(GURLWithIndex(1));
regular_browser_->GetWebStateList()->InsertWebState(
std::move(web_state1),
WebStateList::InsertionParams::Automatic().Pinned());
std::unique_ptr<web::WebState> web_state2 =
CreateFakeWebStateWithURL(GURLWithIndex(2));
regular_browser_->GetWebStateList()->InsertWebState(
std::move(web_state2),
WebStateList::InsertionParams::Automatic().Pinned());
EXPECT_EQ(2UL, consumer_.items.size());
// Inserts one regular and one incognito tab.
std::unique_ptr<web::WebState> web_state3 =
CreateFakeWebStateWithURL(GURLWithIndex(3));
regular_browser_->GetWebStateList()->InsertWebState(
std::move(web_state3), WebStateList::InsertionParams::AtIndex(0));
std::unique_ptr<web::WebState> web_state4 =
CreateFakeWebStateWithURL(GURLWithIndex(4));
incognito_browser_->GetWebStateList()->InsertWebState(
std::move(web_state4), WebStateList::InsertionParams::AtIndex(0));
EXPECT_EQ(2UL, consumer_.items.size());
// Inserts a third pinned tab.
std::unique_ptr<web::WebState> web_state5 =
CreateFakeWebStateWithURL(GURLWithIndex(5));
regular_browser_->GetWebStateList()->InsertWebState(
std::move(web_state5),
WebStateList::InsertionParams::Automatic().Pinned());
EXPECT_EQ(3UL, consumer_.items.size());
}
// Tests that the correct UIDropOperation is returned when dropping tabs in the
// pinned view.
TEST_F(PinnedTabsMediatorTest, DropOperation) {
// The Pinned Tabs feature is not available on iPad.
if (!IsPinnedTabsEnabled()) {
return;
}
// Tests a regular tab.
auto regular_web_state = CreateFakeWebStateWithURL(GURLWithIndex(1));
regular_browser_->GetWebStateList()->InsertWebState(
std::move(regular_web_state), WebStateList::InsertionParams::AtIndex(0));
FakeDropSession* regular_drop_session = FakeDropSessionWithWebState(
regular_browser_->GetWebStateList()->GetWebStateAt(0));
EXPECT_EQ([mediator_ dropOperationForDropSession:regular_drop_session
toIndex:0],
UIDropOperationMove);
// Tests an incognito tab.
auto incognito_web_state = CreateFakeWebStateWithURL(GURLWithIndex(2));
incognito_browser_->GetWebStateList()->InsertWebState(
std::move(incognito_web_state),
WebStateList::InsertionParams::AtIndex(0));
FakeDropSession* incognito_drop_session = FakeDropSessionWithWebState(
incognito_browser_->GetWebStateList()->GetWebStateAt(0));
EXPECT_EQ([mediator_ dropOperationForDropSession:incognito_drop_session
toIndex:0],
UIDropOperationMove);
}
// Tests the drag and drop to reorder webstates.
TEST_F(PinnedTabsMediatorTest, DragAndDropReorder) {
// The Pinned Tabs feature is not available on iPad.
if (!IsPinnedTabsEnabled()) {
return;
}
std::unique_ptr<web::WebState> web_state1 =
CreateFakeWebStateWithURL(GURLWithIndex(1));
web::WebState* web_state_to_move = web_state1.get();
regular_browser_->GetWebStateList()->InsertWebState(
std::move(web_state1),
WebStateList::InsertionParams::Automatic().Pinned());
regular_browser_->GetWebStateList()->InsertWebState(
CreateFakeWebStateWithURL(GURLWithIndex(2)),
WebStateList::InsertionParams::Automatic().Pinned());
regular_browser_->GetWebStateList()->InsertWebState(
CreateFakeWebStateWithURL(GURLWithIndex(3)),
WebStateList::InsertionParams::Automatic().Pinned());
ASSERT_EQ(web_state_to_move,
regular_browser_->GetWebStateList()->GetWebStateAt(0));
UIDragItem* drag_item = CreateTabDragItem(web_state_to_move);
[mediator_ dropItem:drag_item toIndex:2 fromSameCollection:YES];
EXPECT_EQ(web_state_to_move,
regular_browser_->GetWebStateList()->GetWebStateAt(2));
}
// Tests dropping pinned tabs.
TEST_F(PinnedTabsMediatorTest, DropPinnedTabs) {
// The Pinned Tabs feature is not available on iPad.
if (!IsPinnedTabsEnabled()) {
return;
}
WebStateList* web_state_list = regular_browser_->GetWebStateList();
CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"a* b c | d e f", regular_browser_->GetBrowserState()));
// Drop "A" after "C".
web::WebStateID web_state_id =
web_state_list->GetWebStateAt(0)->GetUniqueIdentifier();
id local_object =
[[TabInfo alloc] initWithTabID:web_state_id
browserState:regular_browser_->GetBrowserState()];
NSItemProvider* item_provider = [[NSItemProvider alloc] init];
UIDragItem* drag_item =
[[UIDragItem alloc] initWithItemProvider:item_provider];
drag_item.localObject = local_object;
[mediator_ dropItem:drag_item toIndex:2 fromSameCollection:YES];
EXPECT_EQ("b c a* | d e f", builder.GetWebStateListDescription());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameCollection, 1);
// Drop "C" before "B".
web_state_id = web_state_list->GetWebStateAt(1)->GetUniqueIdentifier();
local_object =
[[TabInfo alloc] initWithTabID:web_state_id
browserState:regular_browser_->GetBrowserState()];
item_provider = [[NSItemProvider alloc] init];
drag_item = [[UIDragItem alloc] initWithItemProvider:item_provider];
drag_item.localObject = local_object;
[mediator_ dropItem:drag_item toIndex:0 fromSameCollection:YES];
EXPECT_EQ("c b a* | d e f", builder.GetWebStateListDescription());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameCollection, 2);
}
// Tests dropping regular tabs .
TEST_F(PinnedTabsMediatorTest, DropRegularTabs) {
// The Pinned Tabs feature is not available on iPad.
if (!IsPinnedTabsEnabled()) {
return;
}
WebStateList* web_state_list = regular_browser_->GetWebStateList();
CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"a* b c | d e f", regular_browser_->GetBrowserState()));
// Drop "E" after "C".
web::WebStateID web_state_id =
web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();
id local_object =
[[TabInfo alloc] initWithTabID:web_state_id
browserState:regular_browser_->GetBrowserState()];
NSItemProvider* item_provider = [[NSItemProvider alloc] init];
UIDragItem* drag_item =
[[UIDragItem alloc] initWithItemProvider:item_provider];
drag_item.localObject = local_object;
[mediator_ dropItem:drag_item toIndex:3 fromSameCollection:NO];
EXPECT_EQ("a* b c e | d f", builder.GetWebStateListDescription());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 1);
// Drop "D" after "E".
web_state_id = web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();
local_object =
[[TabInfo alloc] initWithTabID:web_state_id
browserState:regular_browser_->GetBrowserState()];
item_provider = [[NSItemProvider alloc] init];
drag_item = [[UIDragItem alloc] initWithItemProvider:item_provider];
drag_item.localObject = local_object;
[mediator_ dropItem:drag_item toIndex:4 fromSameCollection:NO];
EXPECT_EQ("a* b c e d | f", builder.GetWebStateListDescription());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 2);
}
// Tests dropping tabs from tab group.
TEST_F(PinnedTabsMediatorTest, DropTabGroupTabs) {
// The Pinned Tabs feature is not available on iPad.
if (!IsPinnedTabsEnabled()) {
return;
}
base::test::ScopedFeatureList scoped_feature_list;
scoped_feature_list.InitAndEnableFeature(kTabGroupsInGrid);
WebStateList* web_state_list = regular_browser_->GetWebStateList();
CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"a* b c | d [ 0 e f ]", regular_browser_->GetBrowserState()));
// Drop "E" after "C".
web::WebStateID web_state_id =
web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();
id local_object =
[[TabInfo alloc] initWithTabID:web_state_id
browserState:regular_browser_->GetBrowserState()];
NSItemProvider* item_provider = [[NSItemProvider alloc] init];
UIDragItem* drag_item =
[[UIDragItem alloc] initWithItemProvider:item_provider];
drag_item.localObject = local_object;
[mediator_ dropItem:drag_item toIndex:3 fromSameCollection:NO];
EXPECT_EQ("a* b c e | d [ 0 f ]", builder.GetWebStateListDescription());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 1);
// Drop "D" after "E".
web_state_id = web_state_list->GetWebStateAt(4)->GetUniqueIdentifier();
local_object =
[[TabInfo alloc] initWithTabID:web_state_id
browserState:regular_browser_->GetBrowserState()];
item_provider = [[NSItemProvider alloc] init];
drag_item = [[UIDragItem alloc] initWithItemProvider:item_provider];
drag_item.localObject = local_object;
[mediator_ dropItem:drag_item toIndex:4 fromSameCollection:NO];
EXPECT_EQ("a* b c e d | [ 0 f ]", builder.GetWebStateListDescription());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 2);
// Drop "F" after "D".
web_state_id = web_state_list->GetWebStateAt(5)->GetUniqueIdentifier();
local_object =
[[TabInfo alloc] initWithTabID:web_state_id
browserState:regular_browser_->GetBrowserState()];
item_provider = [[NSItemProvider alloc] init];
drag_item = [[UIDragItem alloc] initWithItemProvider:item_provider];
drag_item.localObject = local_object;
[mediator_ dropItem:drag_item toIndex:5 fromSameCollection:NO];
EXPECT_EQ("a* b c e d f |", builder.GetWebStateListDescription());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kSameBrowser, 3);
}
// Tests dropping an external URL.
TEST_F(PinnedTabsMediatorTest, DropExternalURL) {
// The Pinned Tabs feature is not available on iPad.
if (!IsPinnedTabsEnabled()) {
return;
}
WebStateList* web_state_list = regular_browser_->GetWebStateList();
CloseAllWebStates(*web_state_list, WebStateList::CLOSE_NO_FLAGS);
WebStateListBuilderFromDescription builder(web_state_list);
ASSERT_TRUE(builder.BuildWebStateListFromDescription(
"a* b c | d", regular_browser_->GetBrowserState()));
ASSERT_EQ(4, web_state_list->count());
NSItemProvider* item_provider = [[NSItemProvider alloc]
initWithContentsOfURL:[NSURL URLWithString:@"https://dragged_url.com"]];
// Drop item after "C".
[mediator_ dropItemFromProvider:item_provider
toIndex:3
placeholderContext:nil];
EXPECT_TRUE(base::test::ios::WaitUntilConditionOrTimeout(
base::Seconds(1), ^bool(void) {
return web_state_list->count() == 5;
}));
web::WebState* web_state = web_state_list->GetWebStateAt(3);
EXPECT_EQ(GURL("https://dragged_url.com"),
web_state->GetNavigationManager()->GetPendingItem()->GetURL());
ExpectThatDragItemOriginMetricLogged(DragItemOrigin::kOther);
}