blob: d3520eff81f6fa688aafc642b74bfd1cd205af3e [file]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/tabs/vertical/tab_collection_node.h"
#include "base/functional/bind.h"
#include "base/test/run_until.h"
#include "build/build_config.h"
#include "build/buildflag.h"
#include "chrome/browser/ui/browser.h"
#include "chrome/browser/ui/browser_tabstrip.h"
#include "chrome/browser/ui/browser_window/public/browser_window_features.h"
#include "chrome/browser/ui/tabs/split_tab_metrics.h"
#include "chrome/browser/ui/tabs/tab_strip_model.h"
#include "chrome/browser/ui/views/frame/browser_view.h"
#include "chrome/browser/ui/views/tabs/browser_tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/vertical/root_tab_collection_node.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_strip_view.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 "content/public/test/browser_test.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/views/controls/scroll_view.h"
#include "ui/views/view.h"
#include "ui/views/view_utils.h"
class TabCollectionNodeBrowserTest
: public VerticalTabsBrowserTestMixin<InProcessBrowserTest> {
public:
TabCollectionNodeBrowserTest() = default;
~TabCollectionNodeBrowserTest() override = default;
protected:
RootTabCollectionNode* root_node() {
return browser()
->GetBrowserView()
.vertical_tab_strip_region_view_for_testing()
->root_node_for_testing();
}
views::View* root_node_view() { return root_node()->view(); }
views::View* parent_view() { return root_node()->view()->parent(); }
TabStripModel* GetTabStripModel() { return browser()->tab_strip_model(); }
// Appends a new unpinned tab to the end of the tab strip.
content::WebContents* AppendTab() {
chrome::AddTabAt(browser(), GURL(url::kAboutBlankURL), /*index=*/-1,
/*foreground=*/true);
return GetTabStripModel()->GetWebContentsAt(GetTabStripModel()->count() -
1);
}
// Inserts a new unpinned tab to the specified index in the tab strip.
content::WebContents* InsertTab(int index) {
chrome::AddTabAt(browser(), GURL(url::kAboutBlankURL), /*index=*/index,
/*foreground=*/true);
return GetTabStripModel()->GetWebContentsAt(index);
}
// Appends a new pinned tab to the end of the pinned tabs.
content::WebContents* AppendPinnedTab() {
chrome::AddTabAt(browser(), GURL(url::kAboutBlankURL),
/*index=*/-1,
/*foreground=*/true,
/*group=*/std::nullopt,
/*pinned=*/true);
return GetTabStripModel()->GetWebContentsAt(
GetTabStripModel()->IndexOfFirstNonPinnedTab() - 1);
}
// Appends a new tab and adds it to a new group.
std::pair<content::WebContents*, tab_groups::TabGroupId>
AppendTabToNewGroup() {
content::WebContents* contents = AppendTab();
const int index = GetTabStripModel()->GetIndexOfWebContents(contents);
const tab_groups::TabGroupId group_id =
GetTabStripModel()->AddToNewGroup({index});
return {contents, group_id};
}
// Appends `num_tabs` new tabs and adds them to a new group.
std::pair<std::vector<content::WebContents*>, tab_groups::TabGroupId>
AppendTabsToNewGroup(int num_tabs) {
std::vector<content::WebContents*> contents;
std::vector<int> indices;
for (int i = 0; i < num_tabs; ++i) {
content::WebContents* wc = AppendTab();
contents.push_back(wc);
indices.push_back(GetTabStripModel()->GetIndexOfWebContents(wc));
}
const tab_groups::TabGroupId group_id =
GetTabStripModel()->AddToNewGroup(indices);
return {contents, group_id};
}
// Appends two new tabs and adds them to a new split group.
std::pair<content::WebContents*, content::WebContents*> AppendSplitTab() {
content::WebContents* contents1 = AppendTab();
content::WebContents* contents2 = AppendTab();
TabStripModel* tab_strip_model = GetTabStripModel();
const int index1 = tab_strip_model->GetIndexOfWebContents(contents1);
const int index2 = tab_strip_model->GetIndexOfWebContents(contents2);
tab_strip_model->ActivateTabAt(
index1, TabStripUserGestureDetails(
TabStripUserGestureDetails::GestureType::kOther));
tab_strip_model->AddToNewSplit(
{index2}, {}, split_tabs::SplitTabCreatedSource::kTabContextMenu);
return {contents1, contents2};
}
// Appends two new pinned tabs and adds them to a new split group.
std::pair<content::WebContents*, content::WebContents*>
AppendPinnedSplitTab() {
content::WebContents* contents1 = AppendPinnedTab();
content::WebContents* contents2 = AppendPinnedTab();
TabStripModel* tab_strip_model = GetTabStripModel();
const int index1 = tab_strip_model->GetIndexOfWebContents(contents1);
const int index2 = tab_strip_model->GetIndexOfWebContents(contents2);
tab_strip_model->ActivateTabAt(
index1, TabStripUserGestureDetails(
TabStripUserGestureDetails::GestureType::kOther));
tab_strip_model->AddToNewSplit(
{index2}, {}, split_tabs::SplitTabCreatedSource::kTabContextMenu);
return {contents1, contents2};
}
};
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_UnpinnedTab) {
AppendTab();
// The root node should contain two nodes: one for pinned, one for unpinned.
ASSERT_EQ(root_node()->children().size(), 2u);
const auto* pinned_node = pinned_collection_node();
ASSERT_NE(pinned_node, nullptr);
const auto* unpinned_node = unpinned_collection_node();
ASSERT_NE(unpinned_node, nullptr);
// The pinned Node should be empty.
ASSERT_EQ(pinned_node->children().size(), 0u);
// The unpinned Node should contain two tabs (the initial one and the new
// one).
ASSERT_EQ(unpinned_node->children().size(), 2u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[1]->type(), TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_PinnedTab) {
AppendPinnedTab();
// The pinned Node should have one tab.
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 1u);
EXPECT_EQ(pinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
// The unpinned Node should have one tab (the initial one).
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 1u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_TabGroup) {
AppendTabToNewGroup();
// The pinned Node should be empty.
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 0u);
// Unpinned Node -> Tab, Group
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 2u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
const auto& group_node = unpinned_node->children()[1];
EXPECT_EQ(group_node->type(), TabCollectionNode::Type::GROUP);
// Group -> Tab
ASSERT_EQ(group_node->children().size(), 1u);
EXPECT_EQ(group_node->children()[0]->type(), TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_MultiTabGroup) {
AppendTabsToNewGroup(2);
// The pinned Node should be empty.
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 0u);
// Unpinned Node -> Tab, Group
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 2u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
const auto& group_node = unpinned_node->children()[1];
EXPECT_EQ(group_node->type(), TabCollectionNode::Type::GROUP);
// Group -> Tab, Tab
ASSERT_EQ(group_node->children().size(), 2u);
EXPECT_EQ(group_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(group_node->children()[1]->type(), TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_SplitTab) {
AppendSplitTab();
// The pinned Node should be empty.
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 0u);
// Unpinned Node -> Tab, Split
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 2u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
const auto& split_node = unpinned_node->children()[1];
EXPECT_EQ(split_node->type(), TabCollectionNode::Type::SPLIT);
// Split -> Tab, Tab
ASSERT_EQ(split_node->children().size(), 2u);
EXPECT_EQ(split_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(split_node->children()[1]->type(), TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_PinnedSplitTab) {
AppendPinnedSplitTab();
// Pinned Node -> Split
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 1u);
const auto& split_node = pinned_node->children()[0];
EXPECT_EQ(split_node->type(), TabCollectionNode::Type::SPLIT);
// Split -> Tab, Tab
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(split_node->children().size(), 2u);
EXPECT_EQ(split_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(split_node->children()[1]->type(), TabCollectionNode::Type::TAB);
// Unpinned Node -> Tab
ASSERT_EQ(unpinned_node->children().size(), 1u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_ViewClasses) {
AppendPinnedTab();
AppendTabToNewGroup();
AppendSplitTab();
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// The root node should contain two nodes: one for pinned
// (VerticalPinnedTabContainerView), one for unpinned
// (VerticalUnpinnedTabContainerView).
EXPECT_TRUE(views::IsViewClass<VerticalTabStripView>(root_node_view()));
const auto* pinned_node = pinned_collection_node();
EXPECT_TRUE(
views::IsViewClass<VerticalPinnedTabContainerView>(pinned_node->view()));
const auto* unpinned_node = unpinned_collection_node();
EXPECT_TRUE(views::IsViewClass<VerticalUnpinnedTabContainerView>(
unpinned_node->view()));
// The pinned Node should be have one tab.
ASSERT_EQ(pinned_node->children().size(), 1u);
EXPECT_EQ(pinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_TRUE(
views::IsViewClass<VerticalTabView>(pinned_node->children()[0]->view()));
// The unpinned Node should contain a tab, a tab group, and a split tab.
ASSERT_EQ(unpinned_node->children().size(), 3u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_TRUE(views::IsViewClass<VerticalTabView>(
unpinned_node->children()[0]->view()));
const auto& group_node = unpinned_node->children()[1];
EXPECT_EQ(group_node->type(), TabCollectionNode::Type::GROUP);
// TODO(crbug.com/442567916): Verify tab group view is created.
ASSERT_EQ(group_node->children().size(), 1u);
EXPECT_EQ(group_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_TRUE(
views::IsViewClass<VerticalTabView>(group_node->children()[0]->view()));
const auto& split_node = unpinned_node->children()[2];
EXPECT_EQ(split_node->type(), TabCollectionNode::Type::SPLIT);
EXPECT_TRUE(views::IsViewClass<VerticalSplitTabView>(split_node->view()));
ASSERT_EQ(split_node->children().size(), 2u);
EXPECT_EQ(split_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_TRUE(
views::IsViewClass<VerticalTabView>(split_node->children()[0]->view()));
EXPECT_EQ(split_node->children()[1]->type(), TabCollectionNode::Type::TAB);
EXPECT_TRUE(
views::IsViewClass<VerticalTabView>(split_node->children()[1]->view()));
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
RootNodePopulatesWithTabs_ViewHierarchy) {
AppendTab();
// The root node should contain two nodes: one for pinned, one for unpinned.
ASSERT_EQ(root_node()->children().size(), 2u);
// The root_node_view should have three children, the pinned and unpinned
// views and a separator.
ASSERT_EQ(root_node_view()->children().size(), 3u);
const auto pinned_node_scroll_view = root_node_view()->children()[0];
ASSERT_TRUE(
views::IsViewClass<views::Separator>(root_node_view()->children()[1]));
const auto unpinned_node_scroll_view = root_node_view()->children()[2];
// The pinned_node_scroll_view's contents should have no children.
ASSERT_TRUE(views::IsViewClass<views::ScrollView>(pinned_node_scroll_view));
ASSERT_EQ(views::AsViewClass<views::ScrollView>(pinned_node_scroll_view)
->contents()
->children()
.size(),
0u);
// The unpinned_node_scroll_view's contents should have two children, the two
// tab views.
ASSERT_TRUE(views::IsViewClass<views::ScrollView>(unpinned_node_scroll_view));
ASSERT_EQ(views::AsViewClass<views::ScrollView>(unpinned_node_scroll_view)
->contents()
->children()
.size(),
2u);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest, GetDirectChildren) {
AppendTab();
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// The root node should contain two nodes: one for pinned, one for unpinned.
ASSERT_EQ(root_node()->children().size(), 2u);
// The root_node_view should have three children, the pinned and unpinned
// views and a separator.
ASSERT_EQ(root_node_view()->children().size(), 3u);
const auto pinned_node_scroll_view = root_node_view()->children()[0];
ASSERT_TRUE(
views::IsViewClass<views::Separator>(root_node_view()->children()[1]));
const auto unpinned_node_scroll_view = root_node_view()->children()[2];
const auto& child_views = root_node()->GetDirectChildren();
ASSERT_EQ(child_views.size(), 2u);
EXPECT_EQ(child_views[0],
views::AsViewClass<views::ScrollView>(pinned_node_scroll_view)
->contents());
EXPECT_EQ(child_views[1],
views::AsViewClass<views::ScrollView>(unpinned_node_scroll_view)
->contents());
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
CollectionReturnsOnlyCollectionItems) {
AppendTab();
views::View* non_collection_view =
parent_view()->AddChildView(std::make_unique<views::View>());
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// The root node should contain two nodes: one for pinned, one for unpinned.
ASSERT_EQ(root_node()->children().size(), 2u);
// The parent_view should have multiple children in addition to the root view.
ASSERT_GE(parent_view()->children().size(), 2u);
views::View* non_collection_view_2 =
root_node_view()->AddChildView(std::make_unique<views::View>());
// The root_node_view should have four children, the pinned and unpinned
// views, a separator and the non-collection view.
ASSERT_EQ(root_node_view()->children().size(), 4u);
const auto& child_views = root_node()->GetDirectChildren();
ASSERT_GE(child_views.size(), 2u);
EXPECT_FALSE(std::ranges::contains(child_views, non_collection_view));
EXPECT_FALSE(std::ranges::contains(child_views, non_collection_view_2));
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
VerticalTabViewIsCreatedForTabs) {
// Add an unpinned tab.
AppendTab();
// Add a pinned tab.
AppendPinnedTab();
// Wait for the root node to populate its children.
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// Verify the pinned node contains a single child.
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 1u);
// Verify that child is a VerticalTabView.
EXPECT_TRUE(
views::IsViewClass<VerticalTabView>(pinned_node->children()[0]->view()));
// Verify the unpinned node contains two children: the initial empty tab and
// the newly appended tab.
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 2u);
// Verify both children are VerticalTabView instances.
EXPECT_TRUE(views::IsViewClass<VerticalTabView>(
unpinned_node->children()[0]->view()));
EXPECT_TRUE(views::IsViewClass<VerticalTabView>(
unpinned_node->children()[1]->view()));
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest, TabsCreatedEvent) {
// The pinned Node should be empty.
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 0u);
// The unpinned Node should have one tab (the initial one).
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 1u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
AppendPinnedTab();
// The pinned Node should have one tab.
ASSERT_EQ(pinned_node->children().size(), 1u);
EXPECT_EQ(pinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
AppendTab();
// The unpinned Node should contain two tabs (the initial one and the new
// one).
ASSERT_EQ(unpinned_node->children().size(), 2u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[1]->type(), TabCollectionNode::Type::TAB);
TabCollectionNode* initial_unpinned_tab_node =
unpinned_node->children()[0].get();
TabCollectionNode* appended_unpinned_tab_node =
unpinned_node->children()[1].get();
// Insert a tab between the two unpinned tabs.
InsertTab(2);
// The unpinned Node should contain three tabs (the initial one, then the new
// one added by InsertTab, then the previous one that was added by AppendTab).
ASSERT_EQ(unpinned_node->children().size(), 3u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[0].get(), initial_unpinned_tab_node);
EXPECT_EQ(unpinned_node->children()[1]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[2]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[2].get(), appended_unpinned_tab_node);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest, CloseTabInteraction) {
// 1. Setup: Have three tabs unpinned.
AppendTab();
AppendTab();
// Wait for the initial structure to be populated.
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// Initial structure: three tabs
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 3u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
// Close a tab.
GetTabStripModel()->DetachAndDeleteWebContentsAt(1);
// After detaching, the unpinned node should only have 2 tabs.
ASSERT_EQ(unpinned_node->children().size(), 2u);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest, DetachAndReattachGroup) {
// 1. Setup: Create an initial tab and a tab group to be detached.
auto [contents_vector, group_id] = AppendTabsToNewGroup(2);
// Wait for the initial structure to be populated.
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// Initial structure: [Tab, GroupA] -> children size 2
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 2u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[1]->type(),
TabCollectionNode::Type::GROUP);
// Detached GroupA node to simulate moving to another window/position.
std::unique_ptr<DetachedTabCollection> detached_group =
GetTabStripModel()->DetachTabGroupForInsertion(group_id);
// After detaching, the unpinned node should only have 1 child (the initial
// tab).
ASSERT_EQ(unpinned_node->children().size(), 1u);
// 3. Re-insert the detached group at index 0.
GetTabStripModel()->InsertDetachedTabGroupAt(std::move(detached_group), 0);
// 4. Verification: The hierarchy should now be updated: [GroupA, Tab].
// Since the detached group was inserted at index 0, it should be the first
// child.
ASSERT_EQ(unpinned_node->children().size(), 2u);
// The first child should now be the Tab Group.
const auto& reinserted_group_node = unpinned_node->children()[0];
EXPECT_EQ(reinserted_group_node->type(), TabCollectionNode::Type::GROUP);
// The second child should be the original Tab.
EXPECT_EQ(unpinned_node->children()[1]->type(), TabCollectionNode::Type::TAB);
// Verify the group itself contains the correct number of children (2 tabs).
ASSERT_EQ(reinserted_group_node->children().size(), 2u);
EXPECT_EQ(reinserted_group_node->children()[0]->type(),
TabCollectionNode::Type::TAB);
EXPECT_EQ(reinserted_group_node->children()[1]->type(),
TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest, GroupContiguousTabs) {
// 1. Setup: Start with three unpinned tabs.
AppendTab(); // Tab 1 (index 1)
AppendTab(); // Tab 2 (index 2)
// Wait for the initial structure to be populated.
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// Initial structure verification: All 3 tabs are direct children of the
// Unpinned Container. [Tab, Tab, Tab] -> children size 3
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 3u);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[1]->type(), TabCollectionNode::Type::TAB);
EXPECT_EQ(unpinned_node->children()[2]->type(), TabCollectionNode::Type::TAB);
GetTabStripModel()->AddToNewGroup({0, 1});
// 2. Verification: The hierarchy should now be updated to [Tab, Group].
// The two tabs are replaced by a single group node.
ASSERT_EQ(unpinned_node->children().size(), 2u);
// The first child is the group.
EXPECT_EQ(unpinned_node->children()[0]->type(),
TabCollectionNode::Type::GROUP);
const auto& new_group_node = unpinned_node->children()[0];
// Verify the group itself contains the correct number of children (2 tabs).
ASSERT_EQ(new_group_node->children().size(), 2u);
EXPECT_EQ(new_group_node->children()[0]->type(),
TabCollectionNode::Type::TAB);
EXPECT_EQ(new_group_node->children()[1]->type(),
TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
SingleMoveWithinCollection) {
// 1. Setup: Start with three unpinned tabs.
AppendTab(); // Tab 1 (index 1)
AppendTab(); // Tab 2 (index 2)
// Wait for the initial structure to be populated.
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// Initial structure verification and saving pointers to nodes.
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 3u);
// A = Initial Tab (index 0)
TabCollectionNode* tab_a_node = unpinned_node->children()[0].get();
// B = Tab 1 (index 1)
TabCollectionNode* tab_b_node = unpinned_node->children()[1].get();
// C = Tab 2 (index 2)
TabCollectionNode* tab_c_node = unpinned_node->children()[2].get();
// Initial Order: [A, B, C]
EXPECT_EQ(unpinned_node->children()[0].get(), tab_a_node);
EXPECT_EQ(unpinned_node->children()[1].get(), tab_b_node);
EXPECT_EQ(unpinned_node->children()[2].get(), tab_c_node);
ui::ListSelectionModel selection_model;
selection_model.AddIndexToSelection(0);
selection_model.set_anchor(std::nullopt);
selection_model.set_active(0);
GetTabStripModel()->SetSelectionFromModel(selection_model);
GetTabStripModel()->MoveSelectedTabsTo(2, std::nullopt);
// 2. Validation: Check the final node order.
ASSERT_EQ(unpinned_node->children().size(), 3u);
// Expected Final Order: [B, C, A]
// Node at index 0 is Tab B (originally index 1).
EXPECT_EQ(unpinned_node->children()[0].get(), tab_b_node);
EXPECT_EQ(unpinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
// Node at index 1 is Tab C (originally index 2).
EXPECT_EQ(unpinned_node->children()[1].get(), tab_c_node);
EXPECT_EQ(unpinned_node->children()[1]->type(), TabCollectionNode::Type::TAB);
// Node at index 2 is Tab A (originally index 0).
EXPECT_EQ(unpinned_node->children()[2].get(), tab_a_node);
EXPECT_EQ(unpinned_node->children()[2]->type(), TabCollectionNode::Type::TAB);
}
IN_PROC_BROWSER_TEST_F(TabCollectionNodeBrowserTest,
SingleMoveAcrossCollection) {
// 1. Setup: Start with three unpinned tabs.
// Tab 0: Initial tab
// Tab 1: AppendTab() -> This is the tab we will pin.
// Tab 2: AppendTab()
AppendTab();
AppendTab();
// Wait for the initial structure to be populated.
ASSERT_TRUE(
base::test::RunUntil([&]() { return !root_node()->children().empty(); }));
// Initial State Validation:
// Pinned: 0 children.
const auto* pinned_node = pinned_collection_node();
ASSERT_EQ(pinned_node->children().size(), 0u);
// Unpinned: 3 children.
const auto* unpinned_node = unpinned_collection_node();
ASSERT_EQ(unpinned_node->children().size(), 3u);
// Save a pointer to the tab we intend to pin (the second one, at index 1).
TabCollectionNode* tab_to_pin_node = unpinned_node->children()[1].get();
// 3. Perform the Move (Pin the tab).
// Pinning the tab at index 1 moves it from the Unpinned collection
// (where it was at index 1) to the Pinned collection (at index 0).
GetTabStripModel()->SetTabPinned(1, true);
// 4. Verification: The RootTabCollectionNode should process the OnNodeMoved
// event. We check that the Pinned container now has the expected child.
ASSERT_TRUE(base::test::RunUntil(
[&]() { return pinned_node->children().size() == 1u; }));
// Final State Validation:
// Pinned Node verification:
// Pinned: 1 child (the moved tab).
ASSERT_EQ(pinned_node->children().size(), 1u);
// The first child of the Pinned node must be the tab we moved.
EXPECT_EQ(pinned_node->children()[0]->type(), TabCollectionNode::Type::TAB);
// Unpinned Node verification:
// Unpinned: 2 children remaining.
ASSERT_EQ(unpinned_node->children().size(), 2u);
// Ensure the tab is no longer a child of the Unpinned node.
EXPECT_NE(unpinned_node->children()[0].get(), tab_to_pin_node);
EXPECT_NE(unpinned_node->children()[1].get(), tab_to_pin_node);
}