blob: 48c17e809f1b50d737982c835ed7902a80077857 [file] [log] [blame]
// 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 "components/tabs/public/tab_collection_observer.h"
#include <memory>
#include <variant>
#include "base/memory/ptr_util.h"
#include "base/memory/raw_ptr.h"
#include "components/tab_groups/tab_group_id.h"
#include "components/tabs/public/mock_tab_group.h"
#include "components/tabs/public/mock_tab_interface.h"
#include "components/tabs/public/pinned_tab_collection.h"
#include "components/tabs/public/tab_collection.h"
#include "components/tabs/public/tab_group.h"
#include "components/tabs/public/tab_group_tab_collection.h"
#include "components/tabs/public/tab_interface.h"
#include "components/tabs/public/tab_strip_collection.h"
#include "components/tabs/public/unpinned_tab_collection.h"
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
namespace tabs {
using testing::Return;
// Mock TabCollectionObserver to record calls for verification.
class MockTabCollectionObserver : public TabCollectionObserver {
public:
MockTabCollectionObserver() = default;
~MockTabCollectionObserver() override = default;
// TabCollectionObserver:
MOCK_METHOD(void,
OnChildrenAdded,
(const TabCollection::Position&, const TabCollectionNodes&),
(override));
MOCK_METHOD(void, OnChildrenRemoved, (const TabCollectionNodes&), (override));
MOCK_METHOD(void,
OnChildMoved,
(const TabCollection::Position& to_position,
const NodeData& node_data),
(override));
};
class TabCollectionObserverTest : public ::testing::Test {
public:
TabCollectionObserverTest() = default;
void SetUp() override {
tab_strip_collection_ = std::make_unique<TabStripCollection>();
tab_strip_collection_->AddObserver(&observer_);
group_factory_ = std::make_unique<MockTabGroupFactory>(nullptr);
}
void TearDown() override {
tab_strip_collection_->RemoveObserver(&observer_);
}
std::unique_ptr<MockTabInterface> CreateMockTab() {
auto tab = std::make_unique<MockTabInterface>();
return tab;
}
TabStripCollection* GetTabstripCollection() {
return tab_strip_collection_.get();
}
MockTabGroupFactory* GetGroupFactory() { return group_factory_.get(); }
MockTabCollectionObserver& GetObserver() { return observer_; }
private:
std::unique_ptr<TabStripCollection> tab_strip_collection_;
MockTabCollectionObserver observer_;
std::unique_ptr<MockTabGroupFactory> group_factory_;
};
TEST_F(TabCollectionObserverTest, OnTabAdded) {
// Setup with one group tab
tab_groups::TabGroupId group_id = tab_groups::TabGroupId::GenerateNew();
EXPECT_CALL(*GetGroupFactory(), Create)
.WillOnce([&](tabs::TabGroupTabCollection* collection,
const tab_groups::TabGroupId& id,
const tab_groups::TabGroupVisualData& visual_data) {
// Return a valid MockTabGroup object.
return std::make_unique<MockTabGroup>(collection, id, visual_data);
});
tabs::TabStripCollection* collection = GetTabstripCollection();
std::unique_ptr<tabs::TabGroupTabCollection> grouped_collection =
std::make_unique<tabs::TabGroupTabCollection>(
*GetGroupFactory(), group_id, tab_groups::TabGroupVisualData());
grouped_collection->AddTab(CreateMockTab(), 0);
collection->InsertTabCollectionAt(std::move(grouped_collection), 0, false,
std::nullopt);
MockTabCollectionObserver& observer = GetObserver();
// Test for unpinned tab.
{
std::unique_ptr<MockTabInterface> tab = CreateMockTab();
TabHandle tab_handle(tab->GetHandle());
TabCollectionNodes expected_handles;
expected_handles.push_back(tab_handle);
const TabCollection::Position expected_position = {
.parent_handle = collection->unpinned_collection()->GetHandle(),
.index = 0ul,
};
EXPECT_CALL(
observer,
OnChildrenAdded(
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(expected_position.parent_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(expected_position.index))),
testing::Eq(expected_handles)))
.Times(1);
collection->AddTabRecursive(std::move(tab), 0, std::nullopt, false);
}
// Test for pinned tab.
{
std::unique_ptr<MockTabInterface> tab = CreateMockTab();
TabHandle tab_handle(tab->GetHandle());
TabCollectionNodes expected_handles;
expected_handles.push_back(tab_handle);
const TabCollection::Position expected_position = {
.parent_handle = collection->pinned_collection()->GetHandle(),
.index = 0ul,
};
EXPECT_CALL(
observer,
OnChildrenAdded(
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(expected_position.parent_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(expected_position.index))),
testing::Eq(expected_handles)))
.Times(1);
collection->AddTabRecursive(std::move(tab), 0, std::nullopt, true);
}
// Test for group tab.
{
std::unique_ptr<MockTabInterface> tab = CreateMockTab();
TabHandle tab_handle(tab->GetHandle());
TabCollectionNodes expected_handles;
expected_handles.push_back(tab_handle);
const TabCollection::Position expected_position = {
.parent_handle =
collection->GetTabGroupCollection(group_id)->GetHandle(),
.index = 1ul,
};
EXPECT_CALL(
observer,
OnChildrenAdded(
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(expected_position.parent_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(expected_position.index))),
testing::Eq(expected_handles)))
.Times(1);
collection->AddTabRecursive(std::move(tab), 3, group_id, false);
}
}
TEST_F(TabCollectionObserverTest, OnTabCollectionAttached) {
MockTabCollectionObserver& observer = GetObserver();
// Setup with one group
tab_groups::TabGroupId group_id = tab_groups::TabGroupId::GenerateNew();
EXPECT_CALL(*GetGroupFactory(), Create)
.WillOnce([&](tabs::TabGroupTabCollection* collection,
const tab_groups::TabGroupId& id,
const tab_groups::TabGroupVisualData& visual_data) {
// Return a valid MockTabGroup object.
return std::make_unique<MockTabGroup>(collection, id, visual_data);
});
tabs::TabStripCollection* collection = GetTabstripCollection();
std::unique_ptr<tabs::TabGroupTabCollection> grouped_collection =
std::make_unique<tabs::TabGroupTabCollection>(
*GetGroupFactory(), group_id, tab_groups::TabGroupVisualData());
grouped_collection->AddTab(CreateMockTab(), 0);
tabs::TabCollectionHandle group_handle = grouped_collection->GetHandle();
TabCollectionNodes expected_handles;
expected_handles.push_back(group_handle);
const TabCollection::Position expected_position = {
.parent_handle = collection->unpinned_collection()->GetHandle(),
.index = 0ul,
};
EXPECT_CALL(
observer,
OnChildrenAdded(
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(expected_position.parent_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(expected_position.index))),
testing::Eq(expected_handles)))
.Times(1);
collection->InsertTabCollectionAt(std::move(grouped_collection), 0, false,
std::nullopt);
// Add a split to the group
split_tabs::SplitTabId split_id = split_tabs::SplitTabId::GenerateNew();
std::unique_ptr<tabs::SplitTabCollection> split_collection_unique =
std::make_unique<tabs::SplitTabCollection>(
split_id, split_tabs::SplitTabVisualData(
split_tabs::SplitTabLayout::kVertical, 0.5));
split_collection_unique->AddTab(CreateMockTab(), 0);
split_collection_unique->AddTab(CreateMockTab(), 0);
tabs::TabCollectionHandle split_handle = split_collection_unique->GetHandle();
TabCollectionNodes expected_handles_split;
expected_handles_split.push_back(split_handle);
const TabCollection::Position expected_position_split = {
.parent_handle = group_handle,
.index = 1ul,
};
EXPECT_CALL(
observer,
OnChildrenAdded(
testing::AllOf(
testing::Field(
&TabCollection::Position::parent_handle,
testing::Eq(expected_position_split.parent_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(expected_position_split.index))),
testing::Eq(expected_handles_split)))
.Times(1);
collection->InsertTabCollectionAt(std::move(split_collection_unique), 1,
false, group_id);
}
TEST_F(TabCollectionObserverTest, OnTabRemoved) {
// Setup with one group tab
tab_groups::TabGroupId group_id = tab_groups::TabGroupId::GenerateNew();
EXPECT_CALL(*GetGroupFactory(), Create)
.WillOnce([&](tabs::TabGroupTabCollection* collection,
const tab_groups::TabGroupId& id,
const tab_groups::TabGroupVisualData& visual_data) {
// Return a valid MockTabGroup object.
return std::make_unique<MockTabGroup>(collection, id, visual_data);
});
std::unique_ptr<tabs::MockTabInterface> pinned_tab = CreateMockTab();
std::unique_ptr<tabs::MockTabInterface> unpinned_tab = CreateMockTab();
std::unique_ptr<tabs::MockTabInterface> group_tab_0 = CreateMockTab();
std::unique_ptr<tabs::MockTabInterface> group_tab_1 = CreateMockTab();
tabs::MockTabInterface* pinned_tab_ptr = pinned_tab.get();
tabs::MockTabInterface* unpinned_tab_ptr = unpinned_tab.get();
tabs::MockTabInterface* group_tab_0_ptr = group_tab_0.get();
tabs::MockTabInterface* group_tab_1_ptr = group_tab_1.get();
TabHandle pinned_tab_handle = pinned_tab->GetHandle();
TabHandle unpinned_tab_handle = unpinned_tab->GetHandle();
TabHandle group_tab_1_handle = group_tab_1->GetHandle();
tabs::TabStripCollection* collection = GetTabstripCollection();
std::unique_ptr<tabs::TabGroupTabCollection> group_collection =
std::make_unique<tabs::TabGroupTabCollection>(
*GetGroupFactory(), group_id, tab_groups::TabGroupVisualData());
tabs::TabGroupTabCollection* group_collection_ptr = group_collection.get();
TabCollectionHandle group_collection_handle = group_collection->GetHandle();
group_collection->AddTab(std::move(group_tab_0), 0);
collection->AddTabRecursive(std::move(pinned_tab), 0, std::nullopt, true);
collection->AddTabRecursive(std::move(unpinned_tab), 1, std::nullopt, false);
collection->InsertTabCollectionAt(std::move(group_collection), 2, false,
std::nullopt);
collection->AddTabRecursive(std::move(group_tab_1), 3, group_id, false);
MockTabCollectionObserver& observer = GetObserver();
// Test for non-final group tab.
{
EXPECT_CALL(*group_tab_1_ptr, GetParentCollection(testing::_))
.WillRepeatedly(Return(group_collection_ptr));
EXPECT_CALL(*group_tab_1_ptr, GetGroup()).WillRepeatedly(Return(group_id));
EXPECT_CALL(observer, OnChildrenRemoved(testing::Eq(
tabs::TabCollectionNodes{group_tab_1_handle})))
.Times(1);
collection->RemoveTabAtIndexRecursive(3);
}
// Test for final group tab.
{
EXPECT_CALL(*group_tab_0_ptr, GetParentCollection(testing::_))
.WillRepeatedly(Return(group_collection_ptr));
EXPECT_CALL(*group_tab_0_ptr, GetGroup()).WillRepeatedly(Return(group_id));
EXPECT_CALL(observer,
OnChildrenRemoved(testing::Eq(
tabs::TabCollectionNodes{group_collection_handle})))
.Times(1);
collection->RemoveTabAtIndexRecursive(2);
}
// Test for unpinned tab.
{
EXPECT_CALL(*unpinned_tab_ptr, GetParentCollection(testing::_))
.WillRepeatedly(Return(collection->unpinned_collection()));
EXPECT_CALL(observer, OnChildrenRemoved(testing::Eq(
tabs::TabCollectionNodes{unpinned_tab_handle})))
.Times(1);
collection->RemoveTabAtIndexRecursive(1);
}
// Test for pinned tab.
{
EXPECT_CALL(*pinned_tab_ptr, GetParentCollection(testing::_))
.WillRepeatedly(Return(collection->pinned_collection()));
EXPECT_CALL(observer, OnChildrenRemoved(testing::Eq(
tabs::TabCollectionNodes{pinned_tab_handle})))
.Times(1);
collection->RemoveTabAtIndexRecursive(0);
}
}
TEST_F(TabCollectionObserverTest, OnCollectionRemoved) {
MockTabCollectionObserver& observer = GetObserver();
tabs::TabStripCollection* collection = GetTabstripCollection();
// Set up a group.
tab_groups::TabGroupId group_id = tab_groups::TabGroupId::GenerateNew();
EXPECT_CALL(*GetGroupFactory(), Create)
.WillOnce([&](tabs::TabGroupTabCollection* collection,
const tab_groups::TabGroupId& id,
const tab_groups::TabGroupVisualData& visual_data) {
// Return a valid MockTabGroup object.
return std::make_unique<MockTabGroup>(collection, id, visual_data);
});
std::unique_ptr<tabs::TabGroupTabCollection> group_collection =
std::make_unique<tabs::TabGroupTabCollection>(
*GetGroupFactory(), group_id, tab_groups::TabGroupVisualData());
tabs::TabGroupTabCollection* group_collection_ptr = group_collection.get();
tabs::TabCollectionHandle group_handle = group_collection->GetHandle();
group_collection->AddTab(CreateMockTab(), 0);
collection->InsertTabCollectionAt(std::move(group_collection), 0, false,
std::nullopt);
// Add a split to the group.
split_tabs::SplitTabId split_id = split_tabs::SplitTabId::GenerateNew();
std::unique_ptr<tabs::SplitTabCollection> split_collection =
std::make_unique<tabs::SplitTabCollection>(
split_id, split_tabs::SplitTabVisualData(
split_tabs::SplitTabLayout::kVertical, 0.5));
tabs::SplitTabCollection* split_collection_ptr = split_collection.get();
tabs::TabCollectionHandle split_handle = split_collection->GetHandle();
split_collection->AddTab(CreateMockTab(), 0);
split_collection->AddTab(CreateMockTab(), 0);
collection->InsertTabCollectionAt(std::move(split_collection), 1, false,
group_id);
// Remove the split.
{
EXPECT_CALL(
observer,
OnChildrenRemoved(testing::Eq(tabs::TabCollectionNodes{split_handle})))
.Times(1);
collection->RemoveTabCollection(split_collection_ptr);
}
// Remove the group.
{
EXPECT_CALL(
observer,
OnChildrenRemoved(testing::Eq(tabs::TabCollectionNodes{group_handle})))
.Times(1);
collection->RemoveTabCollection(group_collection_ptr);
}
}
TEST_F(TabCollectionObserverTest, OnSplitCreated) {
tabs::TabStripCollection* collection = GetTabstripCollection();
// Create 5 tabs
for (int i = 0; i < 5; i++) {
std::unique_ptr<MockTabInterface> tab = CreateMockTab();
EXPECT_CALL(*tab, GetParentCollection(testing::_))
.WillRepeatedly(testing::Return(collection->unpinned_collection()));
collection->AddTabRecursive(std::move(tab), 0, std::nullopt, false);
}
split_tabs::SplitTabId split_id = split_tabs::SplitTabId::GenerateNew();
std::vector<TabInterface*> tabs_to_split = {
collection->GetTabAtIndexRecursive(2),
collection->GetTabAtIndexRecursive(3)};
MockTabCollectionObserver& observer = GetObserver();
TabHandle handle_0 = tabs_to_split[0]->GetHandle();
TabHandle handle_1 = tabs_to_split[1]->GetHandle();
// First Notification: The Split Collection is added to the parent
const TabCollection::Position expected_split_position = {
.parent_handle = collection->unpinned_collection()->GetHandle(),
.index = 2ul,
};
tabs::TabCollectionHandle new_split_handle;
EXPECT_CALL(
observer,
OnChildrenAdded(
testing::AllOf(
testing::Field(
&TabCollection::Position::parent_handle,
testing::Eq(expected_split_position.parent_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(expected_split_position.index))),
testing::SizeIs(1)))
.WillOnce([&](const TabCollection::Position& position,
const TabCollectionNodes& handles) {
// Save the handle of the newly added split collection for the next
// expectation.
new_split_handle = std::get<tabs::TabCollection::Handle>(handles[0]);
EXPECT_EQ(new_split_handle.Get()->type(), TabCollection::Type::SPLIT);
auto src_position_matcher = testing::AllOf(
testing::Field(
&TabCollection::Position::parent_handle,
testing::Eq(collection->unpinned_collection()->GetHandle())),
testing::Field(&TabCollection::Position::index, testing::Eq(3ul)));
EXPECT_CALL(
observer,
OnChildMoved(
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(new_split_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(0))),
testing::AllOf(
testing::Field(&TabCollectionObserver::NodeData::position,
src_position_matcher),
testing::Field(&TabCollectionObserver::NodeData::handle,
testing::VariantWith<TabHandle>(
testing::Eq(handle_0))))))
.Times(1);
EXPECT_CALL(
observer,
OnChildMoved(
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(new_split_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(1))),
testing::AllOf(
testing::Field(&TabCollectionObserver::NodeData::position,
src_position_matcher),
testing::Field(&TabCollectionObserver::NodeData::handle,
testing::VariantWith<TabHandle>(
testing::Eq(handle_1))))))
.Times(1);
});
collection->CreateSplit(split_id, tabs_to_split,
split_tabs::SplitTabVisualData(
split_tabs::SplitTabLayout::kVertical, 0.5));
}
TEST_F(TabCollectionObserverTest, OnUnsplit) {
MockTabCollectionObserver& observer = GetObserver();
tabs::TabStripCollection* collection = GetTabstripCollection();
// Set up a split.
split_tabs::SplitTabId split_id = split_tabs::SplitTabId::GenerateNew();
std::unique_ptr<tabs::SplitTabCollection> split_collection =
std::make_unique<tabs::SplitTabCollection>(
split_id, split_tabs::SplitTabVisualData(
split_tabs::SplitTabLayout::kVertical, 0.5));
tabs::TabCollectionHandle split_handle = split_collection->GetHandle();
for (int i = 0; i < 2; i++) {
std::unique_ptr<MockTabInterface> tab = CreateMockTab();
EXPECT_CALL(*tab, GetParentCollection(testing::_))
.WillRepeatedly(testing::Return(split_collection.get()));
split_collection->AddTab(std::move(tab), i);
}
collection->InsertTabCollectionAt(std::move(split_collection), 0, false,
std::nullopt);
// Unsplit.
{
EXPECT_CALL(
observer,
OnChildMoved(
testing::AllOf(
testing::Field(
&TabCollection::Position::parent_handle,
testing::Eq(
collection->unpinned_collection()->GetHandle())),
testing::Field(&TabCollection::Position::index,
testing::Eq(0))),
testing::AllOf(
testing::Field(
&TabCollectionObserver::NodeData::position,
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(split_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(0ul)))),
testing::Field(
&TabCollectionObserver::NodeData::handle,
testing::VariantWith<TabHandle>(testing::Eq(
collection->GetTabAtIndexRecursive(0)->GetHandle()))))))
.Times(1);
EXPECT_CALL(
observer,
OnChildMoved(
testing::AllOf(
testing::Field(
&TabCollection::Position::parent_handle,
testing::Eq(
collection->unpinned_collection()->GetHandle())),
testing::Field(&TabCollection::Position::index,
testing::Eq(1))),
testing::AllOf(
testing::Field(
&TabCollectionObserver::NodeData::position,
testing::AllOf(
testing::Field(&TabCollection::Position::parent_handle,
testing::Eq(split_handle)),
testing::Field(&TabCollection::Position::index,
testing::Eq(0ul)))),
testing::Field(
&TabCollectionObserver::NodeData::handle,
testing::VariantWith<TabHandle>(testing::Eq(
collection->GetTabAtIndexRecursive(1)->GetHandle()))))))
.Times(1);
EXPECT_CALL(
observer,
OnChildrenRemoved(testing::Eq(tabs::TabCollectionNodes{split_handle})))
.Times(1);
collection->Unsplit(split_id);
}
}
} // namespace tabs