blob: bf14067e1080fc7c2d1d59878534888c9cb774d0 [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include <memory>
#include <vector>
#include "base/test/run_until.h"
#include "chrome/browser/ui/tabs/tab_group_model.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_root_view.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/frame/vertical_tab_strip_region_view.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_pinned_tab_container_view.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_split_tab_view.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_group_view.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_bottom_container.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_strip_top_container.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_tab_view.h"
#include "chrome/browser/ui/views/tabs/vertical/vertical_unpinned_tab_container_view.h"
#include "chrome/browser/ui/views/test/vertical_tabs_browser_test_mixin.h"
#include "chrome/test/base/in_process_browser_test.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tab_groups/tab_group_visual_data.h"
#include "components/tabs/public/tab_group.h"
#include "content/public/test/browser_test.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/gfx/animation/animation_test_api.h"
#include "ui/base/dragdrop/os_exchange_data.h"
#include "ui/gfx/geometry/point_f.h"
#include "ui/views/view_utils.h"
class VerticalTabStripLinkDragTest
: public VerticalTabsBrowserTestMixin<InProcessBrowserTest> {
public:
void EnsureTabCount(int count) {
while (tab_strip_model()->count() < count) {
AppendTab();
}
while (tab_strip_model()->count() > count) {
tab_strip_model()->DetachAndDeleteWebContentsAt(0);
}
ASSERT_EQ(tab_strip_model()->count(), count);
RunScheduledLayouts();
}
VerticalTabStripRegionView* region_view() {
return browser()
->GetBrowserView()
.vertical_tab_strip_region_view_for_testing();
}
VerticalUnpinnedTabContainerView* unpinned_container() {
return region_view()->GetUnpinnedTabsContainer();
}
VerticalPinnedTabContainerView* pinned_container() {
return region_view()->GetPinnedTabsContainer();
}
std::optional<BrowserRootView::DropIndex> GetDropIndexAt(
views::View* view,
gfx::Point loc_in_view) {
const gfx::Point loc_in_region =
views::View::ConvertPointToTarget(view, region_view(), loc_in_view);
ui::OSExchangeData data;
ui::DropTargetEvent event(data, gfx::PointF(loc_in_region),
gfx::PointF(loc_in_region),
ui::DragDropTypes::DRAG_COPY);
return region_view()->GetDropIndex(event);
}
std::vector<VerticalTabView*> WaitForTabs(size_t count) {
std::vector<VerticalTabView*> tab_views;
EXPECT_TRUE(base::test::RunUntil([&]() {
tab_views.clear();
for (views::View* child : unpinned_container()->children()) {
if (auto* tab_view = views::AsViewClass<VerticalTabView>(child)) {
tab_views.push_back(tab_view);
}
}
return tab_views.size() == count &&
(count == 0 || tab_views[0]->height() > 20);
}));
return tab_views;
}
std::vector<VerticalTabView*> WaitForPinnedTabs(size_t count) {
std::vector<VerticalTabView*> tab_views;
EXPECT_TRUE(base::test::RunUntil([&]() {
tab_views.clear();
for (views::View* child : pinned_container()->children()) {
if (auto* tab_view = views::AsViewClass<VerticalTabView>(child)) {
tab_views.push_back(tab_view);
}
}
return tab_views.size() == count &&
(count == 0 || tab_views[0]->height() > 20);
}));
return tab_views;
}
VerticalSplitTabView* WaitForSplitView() {
VerticalSplitTabView* split_view = nullptr;
EXPECT_TRUE(base::test::RunUntil([&]() {
for (views::View* child : unpinned_container()->children()) {
if (auto* v = views::AsViewClass<VerticalSplitTabView>(child)) {
split_view = v;
return v->height() > 20;
}
}
return false;
}));
return split_view;
}
VerticalTabGroupView* WaitForGroupView(bool collapsed = false) {
VerticalTabGroupView* group_view = nullptr;
EXPECT_TRUE(base::test::RunUntil([&]() {
for (views::View* child : unpinned_container()->children()) {
if (auto* v = views::AsViewClass<VerticalTabGroupView>(child)) {
if (v->IsCollapsed() == collapsed &&
v->group_header()->height() >= 26 &&
unpinned_container()->height() > 40) {
group_view = v;
return true;
}
}
}
return false;
}));
return group_view;
}
gfx::AnimationTestApi::RenderModeResetter disable_animation_ =
gfx::AnimationTestApi::SetRichAnimationRenderMode(
gfx::Animation::RichAnimationRenderMode::FORCE_DISABLED);
};
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest, DropBeforeAndAfterTabs) {
EnsureTabCount(3);
auto tab_views = WaitForTabs(3);
// Drop at the top of the first tab.
{
gfx::Point location(tab_views[0]->width() / 2, 2);
auto drop_index = GetDropIndexAt(tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
}
// Drop at the bottom of the first tab.
{
gfx::Point location(tab_views[0]->width() / 2, tab_views[0]->height() - 2);
auto drop_index = GetDropIndexAt(tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 1);
}
// Drop at the bottom of the last tab.
{
gfx::Point location(tab_views[2]->width() / 2, tab_views[2]->height() - 2);
auto drop_index = GetDropIndexAt(tab_views[2], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 3);
}
}
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest, DropInMiddleToReplace) {
EnsureTabCount(3);
auto tab_views = WaitForTabs(3);
// Drop in the middle of the first tab.
{
gfx::Point location(tab_views[0]->width() / 2, tab_views[0]->height() / 2);
auto drop_index = GetDropIndexAt(tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex);
}
// Drop in the middle of the second tab.
{
gfx::Point location(tab_views[1]->width() / 2, tab_views[1]->height() / 2);
auto drop_index = GetDropIndexAt(tab_views[1], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 1);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex);
}
}
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest, DropInSplitTabs) {
AppendSplitTab();
while (tab_strip_model()->count() > 2) {
tab_strip_model()->DetachAndDeleteWebContentsAt(0);
}
ASSERT_EQ(tab_strip_model()->count(), 2);
RunScheduledLayouts();
auto* split_view = WaitForSplitView();
// Drop at the top of the split tab -> before split.
{
gfx::Point location(split_view->width() / 2, 2);
auto drop_index = GetDropIndexAt(split_view, location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kInsertBeforeIndex);
}
// Drop at the bottom of the split tab -> after split.
{
gfx::Point location(split_view->width() / 2, split_view->height() - 2);
auto drop_index = GetDropIndexAt(split_view, location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 2);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kInsertBeforeIndex);
}
// Drop in the middle of the first tab in the split -> replace tab 0.
{
gfx::Point location(split_view->width() / 4, split_view->height() / 2);
auto drop_index = GetDropIndexAt(split_view, location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex);
}
// Drop in the middle of the second tab in the split -> replace tab 1.
{
gfx::Point location(3 * split_view->width() / 4, split_view->height() / 2);
auto drop_index = GetDropIndexAt(split_view, location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 1);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex);
}
// Drop in the middle of the split view horizontally and vertically.
{
// Just to the left of the center point -> replace tab 0.
gfx::Point location(split_view->width() / 2 - 1, split_view->height() / 2);
auto drop_index = GetDropIndexAt(split_view, location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex);
// Just to the right of the center point -> replace tab 1.
location.set_x(split_view->width() / 2 + 1);
drop_index = GetDropIndexAt(split_view, location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 1);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex);
}
}
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest, DropInGroups) {
EnsureTabCount(3);
auto tab_views = WaitForTabs(3);
tab_strip_model()->AddToNewGroup({0, 1});
RunScheduledLayouts();
auto* group_view = WaitForGroupView();
// Drop at the top of the group header -> before group.
{
gfx::Point location(group_view->group_header()->width() / 2, 2);
auto drop_index = GetDropIndexAt(group_view->group_header(), location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
EXPECT_EQ(drop_index->group_inclusion,
BrowserRootView::DropIndex::GroupInclusion::kDontIncludeInGroup);
}
// Drop at the bottom of the group header -> inside group at start.
{
gfx::Point location(group_view->group_header()->width() / 2,
group_view->group_header()->height() - 2);
auto drop_index = GetDropIndexAt(group_view->group_header(), location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
EXPECT_EQ(drop_index->group_inclusion,
BrowserRootView::DropIndex::GroupInclusion::kIncludeInGroup);
}
// Drop at the bottom of the last tab in the group -> ungrouped.
{
gfx::Point location(tab_views[1]->width() / 2, tab_views[1]->height() - 2);
auto drop_index = GetDropIndexAt(tab_views[1], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 2);
EXPECT_EQ(drop_index->group_inclusion,
BrowserRootView::DropIndex::GroupInclusion::kDontIncludeInGroup);
}
}
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest, DropInCollapsedGroups) {
EnsureTabCount(3);
tab_strip_model()->ActivateTabAt(
2, TabStripUserGestureDetails(
TabStripUserGestureDetails::GestureType::kOther));
tab_groups::TabGroupId group_id = tab_strip_model()->AddToNewGroup({0, 1});
const TabGroup* group =
tab_strip_model()->group_model()->GetTabGroup(group_id);
vertical_tab_strip_controller()->ToggleTabGroupCollapsedState(
group, ToggleTabGroupCollapsedStateOrigin::kMenuAction);
RunScheduledLayouts();
auto* group_view = WaitForGroupView(/*collapsed=*/true);
// Drop at the top of the collapsed group header -> before group.
{
gfx::Point location(group_view->group_header()->width() / 2, 2);
auto drop_index = GetDropIndexAt(group_view->group_header(), location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
}
// Drop at the bottom of the collapsed group header -> after group.
{
gfx::Point location(group_view->group_header()->width() / 2,
group_view->group_header()->height() - 2);
auto drop_index = GetDropIndexAt(group_view->group_header(), location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 2);
}
}
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest, DropInPinnedTabs) {
tab_strip_model()->AppendWebContents(
content::WebContents::Create(
content::WebContents::CreateParams(browser()->profile())),
/*foreground=*/true);
tab_strip_model()->SetTabPinned(0, true);
tab_strip_model()->SetTabPinned(1, true);
RunScheduledLayouts();
auto pinned_tab_views = WaitForPinnedTabs(2);
// Drop at the left of the first pinned tab.
{
gfx::Point location(2, pinned_tab_views[0]->height() / 2);
auto drop_index = GetDropIndexAt(pinned_tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
}
// Drop at the right of the first pinned tab.
{
gfx::Point location(pinned_tab_views[0]->width() - 2,
pinned_tab_views[0]->height() / 2);
auto drop_index = GetDropIndexAt(pinned_tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 1);
}
// Drop at the right of the last pinned tab.
{
gfx::Point location(pinned_tab_views[1]->width() - 2,
pinned_tab_views[1]->height() / 2);
auto drop_index = GetDropIndexAt(pinned_tab_views[1], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 2);
}
// Drop in the middle of the first pinned tab -> replace.
{
gfx::Point location(pinned_tab_views[0]->width() / 2,
pinned_tab_views[0]->height() / 2);
auto drop_index = GetDropIndexAt(pinned_tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
EXPECT_EQ(drop_index->relative_to_index,
BrowserRootView::DropIndex::RelativeToIndex::kReplaceIndex);
}
}
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest,
DropInRegionViewOutsideTabStrip) {
EnsureTabCount(3);
RunScheduledLayouts();
{
auto* top_container = region_view()->GetTopContainer();
const gfx::Point loc_in_top(top_container->width() / 2,
top_container->height() / 2);
const gfx::Point loc_in_region = views::View::ConvertPointToTarget(
top_container, region_view(), loc_in_top);
EXPECT_EQ(region_view()->GetDropTarget(loc_in_region), region_view());
EXPECT_EQ(GetDropIndexAt(top_container, loc_in_top)->index, 0);
}
{
auto* bottom_container = region_view()->GetBottomContainer();
const gfx::Point loc_in_bottom(bottom_container->width() / 2,
bottom_container->height() / 2);
const gfx::Point loc_in_region = views::View::ConvertPointToTarget(
bottom_container, region_view(), loc_in_bottom);
EXPECT_EQ(region_view()->GetDropTarget(loc_in_region), region_view());
EXPECT_EQ(GetDropIndexAt(bottom_container, loc_in_bottom)->index, 3);
}
}
IN_PROC_BROWSER_TEST_F(VerticalTabStripLinkDragTest,
DropInPinnedTabsCollapsed) {
tab_strip_model()->AppendWebContents(
content::WebContents::Create(
content::WebContents::CreateParams(browser()->profile())),
/*foreground=*/true);
tab_strip_model()->SetTabPinned(0, true);
tab_strip_model()->SetTabPinned(1, true);
region_view()->OnResize(-1000, true);
RunScheduledLayouts();
auto pinned_tab_views = WaitForPinnedTabs(2);
// Drop at the top of the first pinned tab.
{
gfx::Point location(pinned_tab_views[0]->width() / 2, 2);
auto drop_index = GetDropIndexAt(pinned_tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 0);
}
// Drop at the bottom of the first pinned tab.
{
gfx::Point location(pinned_tab_views[0]->width() / 2,
pinned_tab_views[0]->height() - 2);
auto drop_index = GetDropIndexAt(pinned_tab_views[0], location);
ASSERT_TRUE(drop_index.has_value());
EXPECT_EQ(drop_index->index, 1);
}
}