blob: 7800ea8a3e79f711b1ee25e1933425a01d5fd25e [file] [log] [blame]
// Copyright 2022 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/tab_container_impl.h"
#include <memory>
#include "base/memory/raw_ref.h"
#include "base/ranges/algorithm.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/ui_features.h"
#include "chrome/browser/ui/views/tabs/fake_base_tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/fake_tab_slot_controller.h"
#include "chrome/browser/ui/views/tabs/tab_close_button.h"
#include "chrome/browser/ui/views/tabs/tab_drag_context.h"
#include "chrome/browser/ui/views/tabs/tab_group_header.h"
#include "chrome/browser/ui/views/tabs/tab_group_views.h"
#include "chrome/browser/ui/views/tabs/tab_strip_layout_helper.h"
#include "chrome/browser/ui/views/tabs/tab_strip_types.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "ui/accessibility/ax_node_data.h"
#include "ui/base/dragdrop/drag_drop_types.h"
#include "ui/base/dragdrop/drop_target_event.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/view_utils.h"
#include "ui/views/widget/widget.h"
namespace {
// Walks up the views hierarchy until it finds a tab view. It returns the
// found tab view, or nullptr if none is found.
views::View* FindTabView(views::View* view) {
views::View* current = view;
while (current && !views::IsViewClass<Tab>(current)) {
current = current->parent();
}
return current;
}
class FakeTabDragContext : public TabDragContextBase {
public:
FakeTabDragContext() = default;
~FakeTabDragContext() override = default;
void UpdateAnimationTarget(TabSlotView* tab_slot_view,
const gfx::Rect& target_bounds) override {}
bool IsDragSessionActive() const override { return drag_session_active_; }
bool IsAnimatingDragEnd() const override { return false; }
void CompleteEndDragAnimations() override {}
int GetTabDragAreaWidth() const override { return width(); }
void set_drag_session_active(bool active) { drag_session_active_ = active; }
private:
bool drag_session_active_ = false;
};
class FakeTabContainerController final : public TabContainerController {
public:
explicit FakeTabContainerController(TabStripController& tab_strip_controller)
: tab_strip_controller_(tab_strip_controller) {}
~FakeTabContainerController() override = default;
void set_tab_container(TabContainer* tab_container) {
tab_container_ = tab_container;
}
void set_is_animating_outside_container(bool is_animating_outside_container) {
is_animating_outside_container_ = is_animating_outside_container;
}
bool IsValidModelIndex(int index) const override {
return tab_strip_controller_->IsValidIndex(index);
}
absl::optional<int> GetActiveIndex() const override {
return tab_strip_controller_->GetActiveIndex();
}
int NumPinnedTabsInModel() const override {
for (size_t i = 0;
i < static_cast<size_t>(tab_strip_controller_->GetCount()); ++i) {
if (!tab_strip_controller_->IsTabPinned(static_cast<int>(i)))
return static_cast<int>(i);
}
// All tabs are pinned.
return tab_strip_controller_->GetCount();
}
void OnDropIndexUpdate(absl::optional<int> index, bool drop_before) override {
tab_strip_controller_->OnDropIndexUpdate(index, drop_before);
}
bool IsGroupCollapsed(const tab_groups::TabGroupId& group) const override {
return tab_strip_controller_->IsGroupCollapsed(group);
}
absl::optional<int> GetFirstTabInGroup(
const tab_groups::TabGroupId& group) const override {
return tab_strip_controller_->GetFirstTabInGroup(group);
}
gfx::Range ListTabsInGroup(
const tab_groups::TabGroupId& group) const override {
return tab_strip_controller_->ListTabsInGroup(group);
}
bool CanExtendDragHandle() const override {
return !tab_strip_controller_->IsFrameCondensed() &&
!tab_strip_controller_->EverHasVisibleBackgroundTabShapes();
}
const views::View* GetTabClosingModeMouseWatcherHostView() const override {
return nullptr;
}
bool IsAnimatingInTabStrip() const override {
return tab_container_->IsAnimating() || is_animating_outside_container_;
}
void UpdateAnimationTarget(TabSlotView* tab_slot_view,
gfx::Rect target_bounds) override {}
private:
const raw_ref<TabStripController> tab_strip_controller_;
raw_ptr<const TabContainer, DanglingUntriaged> tab_container_;
// Set this to true to emulate a tab being animated outside `tab_container_`.
bool is_animating_outside_container_ = false;
};
} // namespace
class TabContainerTest : public ChromeViewsTestBase {
public:
TabContainerTest() = default;
TabContainerTest(const TabContainerTest&) = delete;
TabContainerTest& operator=(const TabContainerTest&) = delete;
~TabContainerTest() override = default;
void SetUp() override {
ChromeViewsTestBase::SetUp();
tab_strip_controller_ = std::make_unique<FakeBaseTabStripController>();
tab_container_controller_ = std::make_unique<FakeTabContainerController>(
*(tab_strip_controller_.get()));
tab_slot_controller_ =
std::make_unique<FakeTabSlotController>(tab_strip_controller_.get());
std::unique_ptr<FakeTabDragContext> drag_context =
std::make_unique<FakeTabDragContext>();
std::unique_ptr<TabContainer> tab_container =
std::make_unique<TabContainerImpl>(
*(tab_container_controller_.get()),
nullptr /*hover_card_controller*/, drag_context.get(),
*(tab_slot_controller_.get()), nullptr /*scroll_contents_view*/);
tab_container->SetAvailableWidthCallback(base::BindRepeating(
[](TabContainerTest* test) { return test->tab_container_width_; },
this));
tab_container_controller_->set_tab_container(tab_container.get());
tab_slot_controller_->set_tab_container(tab_container.get());
widget_ = CreateTestWidget();
tab_container_ =
widget_->GetRootView()->AddChildView(std::move(tab_container));
drag_context_ =
widget_->GetRootView()->AddChildView(std::move(drag_context));
SetTabContainerWidth(1000);
}
void TearDown() override {
drag_context_ = nullptr;
tab_container_ = nullptr;
widget_.reset();
tab_slot_controller_.reset();
tab_container_controller_.reset();
tab_strip_controller_.reset();
ChromeViewsTestBase::TearDown();
}
protected:
Tab* AddTab(int model_index,
absl::optional<tab_groups::TabGroupId> group = absl::nullopt,
TabActive active = TabActive::kInactive,
TabPinned pinned = TabPinned::kUnpinned) {
Tab* tab = tab_container_->AddTab(
std::make_unique<Tab>(tab_slot_controller_.get()), model_index, pinned);
tab_strip_controller_->AddTab(model_index, active, pinned);
if (active == TabActive::kActive)
tab_slot_controller_->set_active_tab(tab);
if (group)
AddTabToGroup(model_index, group.value());
return tab;
}
void MoveTab(int from_model_index, int to_model_index) {
tab_strip_controller_->MoveTab(from_model_index, to_model_index);
tab_container_->MoveTab(from_model_index, to_model_index);
}
// Removes the tab from the viewmodel, but leaves the Tab view itself around
// so it can animate closed.
void RemoveTab(int model_index) {
bool was_active =
tab_container_->GetTabAtModelIndex(model_index)->IsActive();
tab_strip_controller_->RemoveTab(model_index);
tab_container_->RemoveTab(model_index, was_active);
}
void AddTabToGroup(int model_index, tab_groups::TabGroupId group) {
tab_container_->GetTabAtModelIndex(model_index)->set_group(group);
tab_strip_controller_->AddTabToGroup(model_index, group);
const auto& group_views = tab_container_->get_group_views_for_testing();
if (group_views.find(group) == group_views.end())
tab_container_->OnGroupCreated(group);
tab_container_->OnGroupMoved(group);
}
void RemoveTabFromGroup(int model_index) {
Tab* tab = tab_container_->GetTabAtModelIndex(model_index);
absl::optional<tab_groups::TabGroupId> old_group = tab->group();
DCHECK(old_group);
tab->set_group(absl::nullopt);
tab_strip_controller_->RemoveTabFromGroup(model_index);
bool group_is_empty = true;
for (int i = 0; i < tab_container_->GetTabCount(); i++) {
if (tab_container_->GetTabAtModelIndex(i)->group() == old_group)
group_is_empty = false;
}
if (group_is_empty) {
tab_container_->OnGroupClosed(old_group.value());
} else {
tab_container_->OnGroupMoved(old_group.value());
}
}
void MoveTabIntoGroup(int index,
absl::optional<tab_groups::TabGroupId> new_group) {
absl::optional<tab_groups::TabGroupId> old_group =
tab_container_->GetTabAtModelIndex(index)->group();
if (old_group.has_value())
RemoveTabFromGroup(index);
if (new_group.has_value())
AddTabToGroup(index, new_group.value());
}
std::vector<TabGroupViews*> ListGroupViews() const {
std::vector<TabGroupViews*> result;
for (auto const& group_view_pair :
tab_container_->get_group_views_for_testing())
result.push_back(group_view_pair.second.get());
return result;
}
// Returns all TabSlotViews in the order that they have as ViewChildren of
// TabContainer. This should match the actual order that they appear in
// visually.
views::View::Views GetTabSlotViewsInFocusOrder() {
views::View::Views all_children = tab_container_->children();
const int num_tab_slot_views =
tab_container_->GetTabCount() +
tab_container_->get_group_views_for_testing().size();
return views::View::Views(all_children.begin(),
all_children.begin() + num_tab_slot_views);
}
// Returns all TabSlotViews in the order that they appear visually. This is
// the expected order of the ViewChildren of TabContainer.
views::View::Views GetTabSlotViewsInVisualOrder() {
views::View::Views ordered_views;
absl::optional<tab_groups::TabGroupId> prev_group = absl::nullopt;
for (int i = 0; i < tab_container_->GetTabCount(); ++i) {
Tab* tab = tab_container_->GetTabAtModelIndex(i);
// If the current Tab is the first one in a group, first add the
// TabGroupHeader to the list of views.
absl::optional<tab_groups::TabGroupId> curr_group = tab->group();
if (curr_group.has_value() && curr_group != prev_group) {
ordered_views.push_back(
tab_container_->GetGroupViews(curr_group.value())->header());
}
prev_group = curr_group;
ordered_views.push_back(tab);
}
return ordered_views;
}
// Makes sure that all tabs have the correct AX indices.
void VerifyTabIndices() {
for (int i = 0; i < tab_container_->GetTabCount(); ++i) {
ui::AXNodeData ax_node_data;
tab_container_->GetTabAtModelIndex(i)
->GetViewAccessibility()
.GetAccessibleNodeData(&ax_node_data);
EXPECT_EQ(i + 1, ax_node_data.GetIntAttribute(
ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(
tab_container_->GetTabCount(),
ax_node_data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
}
// Checks whether |tab| contains |point_in_tab_container_coords|, where the
// point is in |tab_container_| coordinates.
bool IsPointInTab(Tab* tab, const gfx::Point& point_in_tab_container_coords) {
gfx::Point point_in_tab_coords(point_in_tab_container_coords);
views::View::ConvertPointToTarget(tab_container_.get(), tab,
&point_in_tab_coords);
return tab->HitTestPoint(point_in_tab_coords);
}
void SetTabContainerWidth(int width) {
tab_container_width_ = width;
gfx::Size size(tab_container_width_, GetLayoutConstant(TAB_HEIGHT));
widget_->SetSize(size);
drag_context_->SetSize(size);
tab_container_->SetSize(size);
}
// An abridged version of the above that avoids calls to TabContainer::Layout
// from Widget::SetSize.
void SetTabContainerWidthSingleLayout(int width) {
tab_container_width_ = width;
gfx::Size size(tab_container_width_, GetLayoutConstant(TAB_HEIGHT));
tab_container_->SetSize(size);
}
std::unique_ptr<FakeBaseTabStripController> tab_strip_controller_;
std::unique_ptr<FakeTabContainerController> tab_container_controller_;
std::unique_ptr<FakeTabSlotController> tab_slot_controller_;
raw_ptr<FakeTabDragContext> drag_context_;
raw_ptr<TabContainer> tab_container_;
std::unique_ptr<views::Widget> widget_;
int tab_container_width_ = 0;
};
TEST_F(TabContainerTest, ExitsClosingModeAtStandardWidth) {
AddTab(0, absl::nullopt, TabActive::kActive);
// Create just enough tabs so tabs are not full size.
const int standard_width = TabStyle::Get()->GetStandardWidth();
while (tab_container_->GetActiveTabWidth() == standard_width) {
AddTab(0);
tab_container_->CompleteAnimationAndLayout();
}
// The test closes two tabs, we need at least one left over after that.
ASSERT_GE(tab_container_->GetTabCount(), 3);
// Enter tab closing mode manually; this would normally happen as the result
// of a mouse/touch-based tab closure action.
tab_container_->EnterTabClosingMode(absl::nullopt,
CloseTabSource::CLOSE_TAB_FROM_MOUSE);
// Close the second-to-last tab; tab closing mode should remain active,
// constraining tab widths to below full size.
RemoveTab(tab_container_->GetTabCount() - 2);
tab_container_->CompleteAnimationAndLayout();
ASSERT_LT(tab_container_->GetActiveTabWidth(), standard_width);
// Close the last tab; tab closing mode should allow tabs to resize to full
// size.
RemoveTab(tab_container_->GetTabCount() - 1);
tab_container_->CompleteAnimationAndLayout();
EXPECT_EQ(tab_container_->GetActiveTabWidth(), standard_width);
}
// After removing a tab followed by removing a tab in a tabgroup
// Should bring the subsequent tab to its place as expected in
// tab closing mode.
TEST_F(TabContainerTest, RemoveTabInGroupWithTabClosingMode) {
AddTab(0, absl::nullopt, TabActive::kActive);
// Create enough tabs so tabs are not full size.
const int standard_width = TabStyle::Get()->GetStandardWidth();
// Set a tab_counter to avoid infinite loop
int tab_counter = 0;
while ((tab_counter < 100) &&
(tab_container_->GetActiveTabWidth() == standard_width ||
tab_container_->GetTabCount() < 10)) {
AddTab(0);
tab_container_->CompleteAnimationAndLayout();
tab_counter += 1;
}
// add the first two tabs to a group
tab_groups::TabGroupId group1 = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(1, group1);
AddTabToGroup(2, group1);
AddTabToGroup(3, group1);
// Remove the second from last tab
tab_container_->EnterTabClosingMode(absl::nullopt,
CloseTabSource::CLOSE_TAB_FROM_MOUSE);
RemoveTab(tab_container_->GetTabCount() - 2);
tab_container_->CompleteAnimationAndLayout();
// Get the group tab's close button center point
Tab* tab = tab_container_->GetTabAtModelIndex(1);
TabCloseButton* tab_close_button = tab->close_button();
gfx::Point tab_center = tab_close_button->GetBoundsInScreen().CenterPoint();
// Remove the tab
tab_container_->EnterTabClosingMode(absl::nullopt,
CloseTabSource::CLOSE_TAB_FROM_MOUSE);
tab_container_->OnGroupContentsChanged(group1);
RemoveTab(1);
tab_container_->CompleteAnimationAndLayout();
// Check if the next tab moves to its place
Tab* tab_next = tab_container_->GetTabAtModelIndex(1);
raw_ptr<TabCloseButton> tab_next_close_button = tab_next->close_button();
EXPECT_TRUE(tab_next_close_button->GetBoundsInScreen().Contains(tab_center));
}
// Verifies child view order matches model order.
TEST_F(TabContainerTest, TabViewOrder) {
AddTab(0);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
AddTab(1);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
AddTab(2);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
MoveTab(0, 1);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
MoveTab(1, 2);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
MoveTab(1, 0);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
MoveTab(0, 2);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
}
// Verifies child view order matches slot order with group headers.
TEST_F(TabContainerTest, TabViewOrderWithGroups) {
AddTab(0);
AddTab(1);
AddTab(2);
AddTab(3);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
tab_groups::TabGroupId group1 = tab_groups::TabGroupId::GenerateNew();
tab_groups::TabGroupId group2 = tab_groups::TabGroupId::GenerateNew();
// Add multiple tabs to a group and verify view order.
AddTabToGroup(0, group1);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
AddTabToGroup(1, group1);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
// Move tabs within a group and verify view order.
MoveTab(1, 0);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
// Add a single tab to a group and verify view order.
AddTabToGroup(2, group2);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
// Move and add tabs near a group and verify view order.
AddTab(2);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
MoveTab(4, 3);
EXPECT_EQ(GetTabSlotViewsInFocusOrder(), GetTabSlotViewsInVisualOrder());
}
namespace {
ui::DropTargetEvent MakeEventForDragLocation(const gfx::Point& p) {
return ui::DropTargetEvent({}, gfx::PointF(p), {},
ui::DragDropTypes::DRAG_LINK);
}
} // namespace
TEST_F(TabContainerTest, DropIndexForDragLocationIsCorrect) {
auto group = tab_groups::TabGroupId::GenerateNew();
Tab* tab1 = AddTab(0, absl::nullopt, TabActive::kActive);
Tab* tab2 = AddTab(1, group);
Tab* tab3 = AddTab(2, group);
tab_container_->CompleteAnimationAndLayout();
TabGroupHeader* const group_header =
tab_container_->GetGroupViews(group)->header();
using DropIndex = BrowserRootView::DropIndex;
// Check dragging near the edge of each tab.
EXPECT_EQ((DropIndex{0, true, false}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
tab1->bounds().left_center() + gfx::Vector2d(1, 0))));
EXPECT_EQ((DropIndex{1, true, false}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
tab1->bounds().right_center() + gfx::Vector2d(-1, 0))));
EXPECT_EQ((DropIndex{1, true, true}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
tab2->bounds().left_center() + gfx::Vector2d(1, 0))));
EXPECT_EQ((DropIndex{2, true, false}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
tab2->bounds().right_center() + gfx::Vector2d(-1, 0))));
EXPECT_EQ((DropIndex{2, true, false}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
tab3->bounds().left_center() + gfx::Vector2d(1, 0))));
EXPECT_EQ((DropIndex{3, true, false}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
tab3->bounds().right_center() + gfx::Vector2d(-1, 0))));
// Check dragging in the center of each tab.
EXPECT_EQ((DropIndex{0, false, false}),
tab_container_->GetDropIndex(
MakeEventForDragLocation(tab1->bounds().CenterPoint())));
EXPECT_EQ((DropIndex{1, false, false}),
tab_container_->GetDropIndex(
MakeEventForDragLocation(tab2->bounds().CenterPoint())));
EXPECT_EQ((DropIndex{2, false, false}),
tab_container_->GetDropIndex(
MakeEventForDragLocation(tab3->bounds().CenterPoint())));
// Check dragging over group header.
// The left half of the header should drop outside the group.
EXPECT_EQ((DropIndex{1, true, false}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
group_header->bounds().CenterPoint() + gfx::Vector2d(-1, 0))));
// The right half of the header should drop inside the group.
EXPECT_EQ((DropIndex{1, true, true}),
tab_container_->GetDropIndex(MakeEventForDragLocation(
group_header->bounds().CenterPoint() + gfx::Vector2d(1, 0))));
}
TEST_F(TabContainerTest, AccessibilityData) {
// When adding tabs, indices should be set.
AddTab(0);
AddTab(1, absl::nullopt, TabActive::kActive);
VerifyTabIndices();
AddTab(0);
VerifyTabIndices();
RemoveTab(1);
VerifyTabIndices();
MoveTab(1, 0);
VerifyTabIndices();
}
TEST_F(TabContainerTest, GetEventHandlerForOverlappingArea) {
Tab* left_tab = AddTab(0);
Tab* active_tab = AddTab(1, absl::nullopt, TabActive::kActive);
Tab* right_tab = AddTab(2);
Tab* most_right_tab = AddTab(3);
tab_container_->CompleteAnimationAndLayout();
left_tab->SetBoundsRect(gfx::Rect(gfx::Point(0, 0), gfx::Size(200, 20)));
active_tab->SetBoundsRect(gfx::Rect(gfx::Point(150, 0), gfx::Size(200, 20)));
ASSERT_TRUE(active_tab->IsActive());
right_tab->SetBoundsRect(gfx::Rect(gfx::Point(300, 0), gfx::Size(200, 20)));
most_right_tab->SetBoundsRect(
gfx::Rect(gfx::Point(450, 0), gfx::Size(200, 20)));
// Test that active tabs gets events from area in which it overlaps with its
// left neighbour.
gfx::Point left_overlap(
(active_tab->x() + left_tab->bounds().right() + 1) / 2,
active_tab->bounds().bottom() - 1);
// Sanity check that the point is in both active and left tab.
ASSERT_TRUE(IsPointInTab(active_tab, left_overlap));
ASSERT_TRUE(IsPointInTab(left_tab, left_overlap));
EXPECT_EQ(active_tab,
FindTabView(tab_container_->GetEventHandlerForPoint(left_overlap)));
// Test that active tabs gets events from area in which it overlaps with its
// right neighbour.
gfx::Point right_overlap((active_tab->bounds().right() + right_tab->x()) / 2,
active_tab->bounds().bottom() - 1);
// Sanity check that the point is in both active and right tab.
ASSERT_TRUE(IsPointInTab(active_tab, right_overlap));
ASSERT_TRUE(IsPointInTab(right_tab, right_overlap));
EXPECT_EQ(
active_tab,
FindTabView(tab_container_->GetEventHandlerForPoint(right_overlap)));
// Test that if neither of tabs is active, the left one is selected.
gfx::Point unactive_overlap(
(right_tab->x() + most_right_tab->bounds().right() + 1) / 2,
right_tab->bounds().bottom() - 1);
// Sanity check that the point is in both active and left tab.
ASSERT_TRUE(IsPointInTab(right_tab, unactive_overlap));
ASSERT_TRUE(IsPointInTab(most_right_tab, unactive_overlap));
EXPECT_EQ(
right_tab,
FindTabView(tab_container_->GetEventHandlerForPoint(unactive_overlap)));
}
TEST_F(TabContainerTest, GetTooltipHandler) {
Tab* left_tab = AddTab(0);
Tab* active_tab = AddTab(1, absl::nullopt, TabActive::kActive);
Tab* right_tab = AddTab(2);
Tab* most_right_tab = AddTab(3);
tab_container_->CompleteAnimationAndLayout();
// Verify that the active tab will be a tooltip handler for points that hit
// it.
left_tab->SetBoundsRect(gfx::Rect(gfx::Point(0, 0), gfx::Size(200, 20)));
active_tab->SetBoundsRect(gfx::Rect(gfx::Point(150, 0), gfx::Size(200, 20)));
ASSERT_TRUE(active_tab->IsActive());
right_tab->SetBoundsRect(gfx::Rect(gfx::Point(300, 0), gfx::Size(200, 20)));
most_right_tab->SetBoundsRect(
gfx::Rect(gfx::Point(450, 0), gfx::Size(200, 20)));
// Test that active_tab handles tooltips from area in which it overlaps with
// its left neighbour.
gfx::Point left_overlap(
(active_tab->x() + left_tab->bounds().right() + 1) / 2,
active_tab->bounds().bottom() - 1);
// Sanity check that the point is in both active and left tab.
ASSERT_TRUE(IsPointInTab(active_tab, left_overlap));
ASSERT_TRUE(IsPointInTab(left_tab, left_overlap));
EXPECT_EQ(
active_tab,
FindTabView(tab_container_->GetTooltipHandlerForPoint(left_overlap)));
// Test that active_tab handles tooltips from area in which it overlaps with
// its right neighbour.
gfx::Point right_overlap((active_tab->bounds().right() + right_tab->x()) / 2,
active_tab->bounds().bottom() - 1);
// Sanity check that the point is in both active and right tab.
ASSERT_TRUE(IsPointInTab(active_tab, right_overlap));
ASSERT_TRUE(IsPointInTab(right_tab, right_overlap));
EXPECT_EQ(
active_tab,
FindTabView(tab_container_->GetTooltipHandlerForPoint(right_overlap)));
// Test that if neither of tabs is active, the left one is selected.
gfx::Point unactive_overlap(
(right_tab->x() + most_right_tab->bounds().right() + 1) / 2,
right_tab->bounds().bottom() - 1);
// Sanity check that the point is in both tabs.
ASSERT_TRUE(IsPointInTab(right_tab, unactive_overlap));
ASSERT_TRUE(IsPointInTab(most_right_tab, unactive_overlap));
EXPECT_EQ(
right_tab,
FindTabView(tab_container_->GetTooltipHandlerForPoint(unactive_overlap)));
// Confirm that tab strip doe not return tooltip handler for points that
// don't hit it.
EXPECT_FALSE(tab_container_->GetTooltipHandlerForPoint(gfx::Point(-1, 2)));
}
TEST_F(TabContainerTest, GroupHeaderBasics) {
AddTab(0);
Tab* tab = tab_container_->GetTabAtModelIndex(0);
const int first_slot_x = tab->x();
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(0, group);
tab_container_->CompleteAnimationAndLayout();
std::vector<TabGroupViews*> views = ListGroupViews();
EXPECT_EQ(1u, views.size());
TabGroupHeader* header = views[0]->header();
EXPECT_EQ(first_slot_x, header->x());
EXPECT_GT(header->width(), 0);
EXPECT_EQ(header->bounds().right() - TabStyle::Get()->GetTabOverlap(),
tab->x());
EXPECT_EQ(tab->height(), header->height());
}
TEST_F(TabContainerTest, GroupHeaderBetweenTabs) {
AddTab(0);
AddTab(1);
tab_container_->CompleteAnimationAndLayout();
const int second_slot_x = tab_container_->GetTabAtModelIndex(1)->x();
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(1, group);
tab_container_->CompleteAnimationAndLayout();
TabGroupHeader* header = ListGroupViews()[0]->header();
EXPECT_EQ(header->x(), second_slot_x);
}
TEST_F(TabContainerTest, GroupHeaderMovesRightWithTab) {
for (int i = 0; i < 4; i++)
AddTab(i);
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(1, group);
tab_container_->CompleteAnimationAndLayout();
MoveTab(1, 2);
tab_container_->CompleteAnimationAndLayout();
TabGroupHeader* header = ListGroupViews()[0]->header();
// Header is now left of tab 2.
EXPECT_LT(tab_container_->GetTabAtModelIndex(1)->x(), header->x());
EXPECT_LT(header->x(), tab_container_->GetTabAtModelIndex(2)->x());
}
TEST_F(TabContainerTest, GroupHeaderMovesLeftWithTab) {
for (int i = 0; i < 4; i++)
AddTab(i);
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(2, group);
tab_container_->CompleteAnimationAndLayout();
MoveTab(2, 1);
tab_container_->CompleteAnimationAndLayout();
TabGroupHeader* header = ListGroupViews()[0]->header();
// Header is now left of tab 1.
EXPECT_LT(tab_container_->GetTabAtModelIndex(0)->x(), header->x());
EXPECT_LT(header->x(), tab_container_->GetTabAtModelIndex(1)->x());
}
TEST_F(TabContainerTest, GroupHeaderDoesntMoveReorderingTabsInGroup) {
for (int i = 0; i < 4; i++)
AddTab(i);
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(1, group);
AddTabToGroup(2, group);
tab_container_->CompleteAnimationAndLayout();
TabGroupHeader* header = ListGroupViews()[0]->header();
const int initial_header_x = header->x();
Tab* tab1 = tab_container_->GetTabAtModelIndex(1);
const int initial_tab_1_x = tab1->x();
Tab* tab2 = tab_container_->GetTabAtModelIndex(2);
const int initial_tab_2_x = tab2->x();
MoveTab(1, 2);
tab_container_->CompleteAnimationAndLayout();
// Header has not moved.
EXPECT_EQ(initial_header_x, header->x());
EXPECT_EQ(initial_tab_1_x, tab2->x());
EXPECT_EQ(initial_tab_2_x, tab1->x());
}
TEST_F(TabContainerTest, GroupHeaderMovesOnRegrouping) {
for (int i = 0; i < 3; i++)
AddTab(i);
tab_groups::TabGroupId group0 = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(0, group0);
tab_groups::TabGroupId group1 = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(1, group1);
AddTabToGroup(2, group1);
tab_container_->CompleteAnimationAndLayout();
std::vector<TabGroupViews*> views = ListGroupViews();
auto views_it = base::ranges::find(views, group1, [](TabGroupViews* view) {
return view->header()->group();
});
ASSERT_TRUE(views_it != views.end());
TabGroupViews* group1_views = *views_it;
// Change groups in a way so that the header should swap with the tab, without
// an explicit MoveTab call.
MoveTabIntoGroup(1, group0);
tab_container_->CompleteAnimationAndLayout();
// Header is now right of tab 1.
EXPECT_LT(tab_container_->GetTabAtModelIndex(1)->x(),
group1_views->header()->x());
EXPECT_LT(group1_views->header()->x(),
tab_container_->GetTabAtModelIndex(2)->x());
}
TEST_F(TabContainerTest, UngroupedTabMovesLeftOfHeader) {
for (int i = 0; i < 2; i++)
AddTab(i);
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(0, group);
tab_container_->CompleteAnimationAndLayout();
MoveTab(1, 0);
tab_container_->CompleteAnimationAndLayout();
// Header is right of tab 0.
TabGroupHeader* header = ListGroupViews()[0]->header();
EXPECT_LT(tab_container_->GetTabAtModelIndex(0)->x(), header->x());
EXPECT_LT(header->x(), tab_container_->GetTabAtModelIndex(1)->x());
}
TEST_F(TabContainerTest, DeleteTabGroupViewsWhenEmpty) {
AddTab(0);
AddTab(1);
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(0, group);
AddTabToGroup(1, group);
RemoveTabFromGroup(0);
EXPECT_EQ(1u, ListGroupViews().size());
RemoveTabFromGroup(1);
EXPECT_EQ(0u, ListGroupViews().size());
}
TEST_F(TabContainerTest, GroupUnderlineBasics) {
AddTab(0);
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(0, group);
tab_container_->CompleteAnimationAndLayout();
std::vector<TabGroupViews*> views = ListGroupViews();
EXPECT_EQ(1u, views.size());
// Update underline manually in the absence of a real Paint cycle.
views[0]->UpdateBounds();
const TabGroupUnderline* underline = views[0]->underline();
EXPECT_EQ(underline->x(), TabGroupUnderline::GetStrokeInset());
EXPECT_GT(underline->width(), 0);
EXPECT_EQ(underline->bounds().right(),
tab_container_->GetTabAtModelIndex(0)->bounds().right() -
TabGroupUnderline::GetStrokeInset());
EXPECT_EQ(underline->height(), TabGroupUnderline::kStrokeThickness);
// Endpoints are different if the last grouped tab is active.
AddTab(1, absl::nullopt, TabActive::kActive);
MoveTabIntoGroup(1, group);
tab_container_->CompleteAnimationAndLayout();
views[0]->UpdateBounds();
EXPECT_EQ(underline->x(), TabGroupUnderline::GetStrokeInset());
EXPECT_EQ(underline->bounds().right(),
tab_container_->GetTabAtModelIndex(1)->bounds().right() +
TabGroupUnderline::kStrokeThickness);
}
TEST_F(TabContainerTest, UnderlineBoundsTabVisibilityChange) {
// Validates that group underlines are updated correctly in a single Layout
// call when the visibility of tabs in the group change. See crbug.com/1356177
// This test is only valid with scrolling off, since it pertains to tab
// visibility stuff that scrolling doesn't do.
ASSERT_FALSE(base::FeatureList::IsEnabled(features::kScrollableTabStrip));
SetTabContainerWidth(200);
// Add tabs to a single group until the last one is not visible.
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
do {
AddTab(0, group);
tab_container_->CompleteAnimationAndLayout();
} while (tab_container_->GetTabAtModelIndex(tab_container_->GetTabCount() - 1)
->GetVisible());
const TabGroupUnderline* underline = ListGroupViews()[0]->underline();
const gfx::Rect initial_bounds = underline->bounds();
// Shrink the TabContainer and verify that the underline bounds changed. Use
// the abridged version of the method to ensure TabContainer::Layout is called
// exactly once.
SetTabContainerWidthSingleLayout(100);
const gfx::Rect shrunk_bounds = underline->bounds();
EXPECT_NE(shrunk_bounds, initial_bounds);
// Re-expand the TabContainer and verify that the underline bounds changed.
// Use the abridged version of the method to ensure TabContainer::Layout is
// called exactly once.
SetTabContainerWidthSingleLayout(300);
EXPECT_NE(underline->bounds(), initial_bounds);
EXPECT_NE(underline->bounds(), shrunk_bounds);
}
TEST_F(TabContainerTest, UnderlineBoundsCollapsedGroupHeaderVisibilityChange) {
// Validates that group underlines are updated correctly in a single Layout
// call when the visibility of the group header changes, even if the group is
// collapsed. See crbug.com/1374614
// This test is only valid with scrolling off, since it pertains to tab
// visibility stuff that scrolling doesn't do.
ASSERT_FALSE(base::FeatureList::IsEnabled(features::kScrollableTabStrip));
SetTabContainerWidth(200);
// Create a tab group with one tab and collapse it.
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTab(0, absl::nullopt, TabActive::kActive);
AddTab(1, group);
tab_strip_controller_->ToggleTabGroupCollapsedState(
group, ToggleTabGroupCollapsedStateOrigin::kMouse);
// Add tabs until the group header is not visible.
do {
AddTab(0);
tab_container_->CompleteAnimationAndLayout();
} while (ListGroupViews()[0]->header()->GetVisible());
const TabGroupUnderline* underline = ListGroupViews()[0]->underline();
const gfx::Rect initial_bounds = underline->bounds();
// Expand the TabContainer and verify that the underline bounds changed.
// Use the abridged version of the method to ensure TabContainer::Layout is
// called exactly once.
SetTabContainerWidthSingleLayout(300);
EXPECT_NE(underline->bounds(), initial_bounds);
}
TEST_F(TabContainerTest, GroupHighlightBasics) {
AddTab(0);
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(0, group);
tab_container_->CompleteAnimationAndLayout();
std::vector<TabGroupViews*> views = ListGroupViews();
EXPECT_EQ(1u, views.size());
// The highlight bounds match the group view bounds. Grab this manually
// here, since there isn't a real paint cycle to trigger OnPaint().
gfx::Rect bounds = views[0]->GetBounds();
EXPECT_EQ(bounds.x(), 0);
EXPECT_GT(bounds.width(), 0);
EXPECT_EQ(bounds.right(),
tab_container_->GetTabAtModelIndex(0)->bounds().right());
EXPECT_EQ(bounds.height(),
tab_container_->GetTabAtModelIndex(0)->bounds().height());
}
TEST_F(TabContainerTest, PreferredWidthDuringAnimation) {
AddTab(0);
AddTab(0);
const int initial_pref_width = tab_container_->GetPreferredSize().width();
// Trigger an animation.
RemoveTab(0);
ASSERT_TRUE(tab_container_->IsAnimating());
// During animations, container preferred size should animate smoothly.
EXPECT_EQ(initial_pref_width, tab_container_->GetPreferredSize().width());
// Minimum size should match preferred width during animations.
EXPECT_EQ(tab_container_->GetPreferredSize().width(),
tab_container_->GetMinimumSize().width());
// Complete the animation and the preferred width should match ideal bounds of
// the trailingmost tab.
tab_container_->CompleteAnimationAndLayout();
ASSERT_NE(initial_pref_width, tab_container_->GetPreferredSize().width());
EXPECT_EQ(tab_container_->GetPreferredSize().width(),
tab_container_->GetIdealBounds(tab_container_->GetTabCount() - 1)
.right());
}
TEST_F(TabContainerTest, PreferredWidthNotAffectedByTransferTabTo) {
// Start with two tabs.
AddTab(0);
AddTab(1);
const int initial_pref_width = tab_container_->GetPreferredSize().width();
// Transfer one out, then pretend to animate it.
std::unique_ptr<views::View> hold_my_tab = std::make_unique<views::View>();
hold_my_tab->AddChildView(tab_container_->RemoveTabFromViewModel(1));
tab_container_controller_->set_is_animating_outside_container(true);
// Preferred width should be unchanged, even though `owned_tab` is no longer
// part of `tab_container_`.
EXPECT_EQ(initial_pref_width, tab_container_->GetPreferredSize().width());
// Minimum size should match preferred width during animations.
EXPECT_EQ(tab_container_->GetPreferredSize().width(),
tab_container_->GetMinimumSize().width());
// Complete the animation and stop pretending.
tab_container_->CompleteAnimationAndLayout();
tab_container_controller_->set_is_animating_outside_container(false);
// Preferred width should now be changed.
EXPECT_NE(initial_pref_width, tab_container_->GetPreferredSize().width());
}
TEST_F(TabContainerTest, PreferredWidthAddTabToViewModel) {
// Start with one tab, and one more that is not in the container.
AddTab(0);
const auto owned_tab = std::make_unique<Tab>(tab_slot_controller_.get());
const int initial_pref_width = tab_container_->GetPreferredSize().width();
// Add `owned_tab` to `tab_container_`'s viewmodel without giving it the
// actual view, and pretend to animate it.
tab_container_->AddTabToViewModel(owned_tab.get(), 1, TabPinned::kUnpinned);
tab_container_controller_->set_is_animating_outside_container(true);
// Preferred width should be unchanged.
EXPECT_EQ(initial_pref_width, tab_container_->GetPreferredSize().width());
// Minimum size should match preferred width during animations.
EXPECT_EQ(tab_container_->GetPreferredSize().width(),
tab_container_->GetMinimumSize().width());
// Complete animation and stop pretending.
tab_container_->CompleteAnimationAndLayout();
tab_container_controller_->set_is_animating_outside_container(false);
// Preferred width should be changed, even though we still haven't handed the
// actual view over.
EXPECT_NE(initial_pref_width, tab_container_->GetPreferredSize().width());
}
TEST_F(TabContainerTest, TabDestroyedWhileOutOfContainerDoesNotActuallyReturn) {
// Add a tab, but take the view to simulate an outside-container animation.
std::unique_ptr<views::View> tab_parent = std::make_unique<views::View>();
Tab* tab_ptr =
tab_parent->AddChildView(tab_container_->RemoveChildViewT(AddTab(0)));
// Simulate destroying the tabstrip during this animation:
// 1. Close the tab.
RemoveTab(0);
// 2. Remove it from the view hierarchy (this would happen as part of the
// tab's destructor).
std::unique_ptr<Tab> tab = tab_parent->RemoveChildViewT(tab_ptr);
// 3. BoundsAnimator completes the animation, which returns the TabSlotView.
tab_container_->ReturnTabSlotView(tab_ptr);
// Validate that `tab_container_` did not actually take the tab view back.
EXPECT_EQ(tab->parent(), nullptr);
}
TEST_F(TabContainerTest, GetLeadingTrailingElementsForZOrdering) {
// An empty TabContainer has no leading/trailing views.
EXPECT_EQ(tab_container_->GetLeadingElementForZOrdering(), absl::nullopt);
EXPECT_EQ(tab_container_->GetTrailingElementForZOrdering(), absl::nullopt);
// Leading/trailing views could be tabs.
Tab* const first_tab = AddTab(0);
Tab* const last_tab = AddTab(1);
EXPECT_EQ(tab_container_->GetLeadingElementForZOrdering()->view(), first_tab);
EXPECT_EQ(tab_container_->GetTrailingElementForZOrdering()->view(), last_tab);
// Leading view could be a group header.
tab_groups::TabGroupId group = tab_groups::TabGroupId::GenerateNew();
AddTabToGroup(0, group);
AddTabToGroup(1, group);
TabGroupHeader* const group_header =
tab_container_->GetGroupViews(group)->header();
EXPECT_EQ(tab_container_->GetLeadingElementForZOrdering()->view(),
group_header);
}