blob: 65827b3bea3978aeabc9f8d019a8859bfe65933a [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// 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_strip.h"
#include <string>
#include "base/bind.h"
#include "base/macros.h"
#include "base/timer/timer.h"
#include "chrome/browser/ui/layout_constants.h"
#include "chrome/browser/ui/tabs/tab_group_id.h"
#include "chrome/browser/ui/tabs/tab_style.h"
#include "chrome/browser/ui/views/tabs/fake_base_tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/new_tab_button.h"
#include "chrome/browser/ui/views/tabs/tab.h"
#include "chrome/browser/ui/views/tabs/tab_group_header.h"
#include "chrome/browser/ui/views/tabs/tab_icon.h"
#include "chrome/browser/ui/views/tabs/tab_renderer_data.h"
#include "chrome/browser/ui/views/tabs/tab_strip_controller.h"
#include "chrome/browser/ui/views/tabs/tab_strip_observer.h"
#include "chrome/browser/ui/views/tabs/tab_style_views.h"
#include "chrome/test/base/testing_profile.h"
#include "chrome/test/views/chrome_views_test_base.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/base/material_design/material_design_controller.h"
#include "ui/base/test/material_design_controller_test_api.h"
#include "ui/events/base_event_utils.h"
#include "ui/gfx/animation/animation_test_api.h"
#include "ui/gfx/canvas.h"
#include "ui/gfx/geometry/rect_conversions.h"
#include "ui/gfx/skia_util.h"
#include "ui/views/accessibility/ax_event_manager.h"
#include "ui/views/accessibility/ax_event_observer.h"
#include "ui/views/accessibility/view_accessibility.h"
#include "ui/views/controls/label.h"
#include "ui/views/view.h"
#include "ui/views/view_targeter.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, on NULL if none is found.
views::View* FindTabView(views::View* view) {
views::View* current = view;
while (current && strcmp(current->GetClassName(), Tab::kViewClassName)) {
current = current->parent();
}
return current;
}
class TestAXEventObserver : public views::AXEventObserver {
public:
TestAXEventObserver() { views::AXEventManager::Get()->AddObserver(this); }
~TestAXEventObserver() override {
views::AXEventManager::Get()->RemoveObserver(this);
}
// views::AXEventObserver:
void OnViewEvent(views::View* view, ax::mojom::Event event_type) override {
if (event_type == ax::mojom::Event::kSelectionRemove) {
remove_count_++;
}
if (event_type == ax::mojom::Event::kSelection) {
change_count_++;
}
if (event_type == ax::mojom::Event::kSelectionAdd) {
add_count_++;
}
}
int add_count() { return add_count_; }
int change_count() { return change_count_; }
int remove_count() { return remove_count_; }
private:
int add_count_ = 0;
int change_count_ = 0;
int remove_count_ = 0;
DISALLOW_COPY_AND_ASSIGN(TestAXEventObserver);
};
} // namespace
class TestTabStripObserver : public TabStripObserver {
public:
explicit TestTabStripObserver(TabStrip* tab_strip) : tab_strip_(tab_strip) {
tab_strip_->AddObserver(this);
}
~TestTabStripObserver() override { tab_strip_->RemoveObserver(this); }
int last_tab_added() const { return last_tab_added_; }
int last_tab_removed() const { return last_tab_removed_; }
int last_tab_moved_from() const { return last_tab_moved_from_; }
int last_tab_moved_to() const { return last_tab_moved_to_; }
private:
// TabStripObserver overrides.
void OnTabAdded(int index) override { last_tab_added_ = index; }
void OnTabMoved(int from_index, int to_index) override {
last_tab_moved_from_ = from_index;
last_tab_moved_to_ = to_index;
}
void OnTabRemoved(int index) override { last_tab_removed_ = index; }
TabStrip* tab_strip_;
int last_tab_added_ = -1;
int last_tab_removed_ = -1;
int last_tab_moved_from_ = -1;
int last_tab_moved_to_ = -1;
DISALLOW_COPY_AND_ASSIGN(TestTabStripObserver);
};
class TabStripTest : public ChromeViewsTestBase,
public testing::WithParamInterface<bool> {
public:
TabStripTest()
: test_api_(GetParam()),
animation_mode_reset_(gfx::AnimationTestApi::SetRichAnimationRenderMode(
gfx::Animation::RichAnimationRenderMode::FORCE_ENABLED)) {}
~TabStripTest() override {}
void SetUp() override {
ChromeViewsTestBase::SetUp();
controller_ = new FakeBaseTabStripController;
tab_strip_ = new TabStrip(std::unique_ptr<TabStripController>(controller_));
controller_->set_tab_strip(tab_strip_);
// Do this to force TabStrip to create the buttons.
auto* parent = new views::View;
parent->AddChildView(tab_strip_);
widget_.reset(new views::Widget);
views::Widget::InitParams init_params =
CreateParams(views::Widget::InitParams::TYPE_POPUP);
init_params.ownership =
views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET;
init_params.bounds = gfx::Rect(0, 0, 200, 200);
widget_->Init(init_params);
widget_->SetContentsView(parent);
}
void TearDown() override {
widget_.reset();
ChromeViewsTestBase::TearDown();
}
protected:
bool IsShowingAttentionIndicator(Tab* tab) {
return tab->icon_->ShowingAttentionIndicator();
}
// Checks whether |tab| contains |point_in_tabstrip_coords|, where the point
// is in |tab_strip_| coordinates.
bool IsPointInTab(Tab* tab, const gfx::Point& point_in_tabstrip_coords) {
gfx::Point point_in_tab_coords(point_in_tabstrip_coords);
views::View::ConvertPointToTarget(tab_strip_, tab, &point_in_tab_coords);
return tab->HitTestPoint(point_in_tab_coords);
}
Tab* FindTabForEvent(const gfx::Point& point) {
return tab_strip_->FindTabForEvent(point);
}
void CompleteAnimationAndLayout() {
tab_strip_->CompleteAnimationAndLayout();
}
void AnimateToIdealBounds() { tab_strip_->AnimateToIdealBounds(); }
const StackedTabStripLayout* touch_layout() const {
return tab_strip_->touch_layout_.get();
}
views::BoundsAnimator* bounds_animator() {
return &tab_strip_->bounds_animator_;
}
int GetActiveTabWidth() { return tab_strip_->GetActiveTabWidth(); }
int GetInactiveTabWidth() { return tab_strip_->GetInactiveTabWidth(); }
// End any outstanding drag and animate tabs back to their ideal bounds.
void StopDraggingTab(Tab* tab) {
// Passing false for |is_first_tab| results in running the post-drag
// animation unconditionally.
bool is_first_tab = false;
tab_strip_->StoppedDraggingTab(tab, &is_first_tab);
}
// Makes sure that all tabs have the correct AX indices.
void VerifyTabIndices() {
for (int i = 0; i < tab_strip_->tab_count(); ++i) {
ui::AXNodeData ax_node_data;
tab_strip_->tab_at(i)->GetViewAccessibility().GetAccessibleNodeData(
&ax_node_data);
EXPECT_EQ(i + 1, ax_node_data.GetIntAttribute(
ax::mojom::IntAttribute::kPosInSet));
EXPECT_EQ(
tab_strip_->tab_count(),
ax_node_data.GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
}
}
std::vector<TabGroupHeader*> ListGroupHeaders() const {
std::vector<TabGroupHeader*> result;
for (auto const& header_pair : tab_strip_->group_headers_)
result.push_back(header_pair.second.get());
return result;
}
// Owned by TabStrip.
FakeBaseTabStripController* controller_ = nullptr;
TabStrip* tab_strip_ = nullptr;
std::unique_ptr<views::Widget> widget_;
ui::MouseEvent dummy_event_ = ui::MouseEvent(ui::ET_MOUSE_PRESSED,
gfx::PointF(),
gfx::PointF(),
base::TimeTicks::Now(),
0,
0);
private:
ui::test::MaterialDesignControllerTestAPI test_api_;
std::unique_ptr<base::AutoReset<gfx::Animation::RichAnimationRenderMode>>
animation_mode_reset_;
DISALLOW_COPY_AND_ASSIGN(TabStripTest);
};
TEST_P(TabStripTest, GetModelCount) {
EXPECT_EQ(0, tab_strip_->GetModelCount());
}
TEST_P(TabStripTest, AccessibilityEvents) {
TestAXEventObserver observer;
// When adding tabs, SetSelection() is called after AddTabAt(), as
// otherwise the index would not be meaningful.
tab_strip_->AddTabAt(0, TabRendererData(), false);
tab_strip_->AddTabAt(1, TabRendererData(), true);
ui::ListSelectionModel selection;
selection.SetSelectedIndex(1);
tab_strip_->SetSelection(selection);
EXPECT_EQ(0, observer.add_count());
EXPECT_EQ(1, observer.change_count());
EXPECT_EQ(0, observer.remove_count());
// When removing tabs, SetSelection() is called before RemoveTabAt(), as
// otherwise the index would not be meaningful.
selection.SetSelectedIndex(0);
tab_strip_->SetSelection(selection);
tab_strip_->RemoveTabAt(nullptr, 1, true);
EXPECT_EQ(0, observer.add_count());
EXPECT_EQ(2, observer.change_count());
EXPECT_EQ(0, observer.remove_count());
// When activating widget, refire selection event on tab.
widget_->OnNativeWidgetActivationChanged(true);
EXPECT_EQ(0, observer.add_count());
EXPECT_EQ(3, observer.change_count());
EXPECT_EQ(0, observer.remove_count());
}
TEST_P(TabStripTest, AccessibilityData) {
// When adding tabs, indexes should be set.
tab_strip_->AddTabAt(0, TabRendererData(), false);
tab_strip_->AddTabAt(1, TabRendererData(), true);
VerifyTabIndices();
tab_strip_->AddTabAt(0, TabRendererData(), false);
VerifyTabIndices();
tab_strip_->RemoveTabAt(nullptr, 1, false);
VerifyTabIndices();
tab_strip_->MoveTab(1, 0, TabRendererData());
VerifyTabIndices();
}
TEST_P(TabStripTest, IsValidModelIndex) {
EXPECT_FALSE(tab_strip_->IsValidModelIndex(0));
}
TEST_P(TabStripTest, tab_count) {
EXPECT_EQ(0, tab_strip_->tab_count());
}
TEST_P(TabStripTest, AddTabAt) {
TestTabStripObserver observer(tab_strip_);
tab_strip_->AddTabAt(0, TabRendererData(), false);
ASSERT_EQ(1, tab_strip_->tab_count());
EXPECT_EQ(0, observer.last_tab_added());
Tab* tab = tab_strip_->tab_at(0);
EXPECT_FALSE(tab == NULL);
}
TEST_P(TabStripTest, MoveTab) {
TestTabStripObserver observer(tab_strip_);
tab_strip_->AddTabAt(0, TabRendererData(), false);
tab_strip_->AddTabAt(1, TabRendererData(), false);
tab_strip_->AddTabAt(2, TabRendererData(), false);
ASSERT_EQ(3, tab_strip_->tab_count());
EXPECT_EQ(2, observer.last_tab_added());
Tab* tab = tab_strip_->tab_at(0);
tab_strip_->MoveTab(0, 1, TabRendererData());
EXPECT_EQ(0, observer.last_tab_moved_from());
EXPECT_EQ(1, observer.last_tab_moved_to());
EXPECT_EQ(tab, tab_strip_->tab_at(1));
}
// Verifies child views are deleted after an animation completes.
TEST_P(TabStripTest, RemoveTab) {
TestTabStripObserver observer(tab_strip_);
controller_->AddTab(0, false);
controller_->AddTab(1, false);
const size_t num_children = tab_strip_->children().size();
EXPECT_EQ(2, tab_strip_->tab_count());
controller_->RemoveTab(0);
EXPECT_EQ(0, observer.last_tab_removed());
// When removing a tab the tabcount should immediately decrement.
EXPECT_EQ(1, tab_strip_->tab_count());
// But the number of views should remain the same (it's animatining closed).
EXPECT_EQ(num_children, tab_strip_->children().size());
tab_strip_->SetBounds(0, 0, 200, 20);
// Layout at a different size should force the animation to end and delete
// the tab that was removed.
tab_strip_->Layout();
EXPECT_EQ(num_children - 1, tab_strip_->children().size());
// Remove the last tab to make sure things are cleaned up correctly when
// the TabStrip is destroyed and an animation is ongoing.
controller_->RemoveTab(0);
EXPECT_EQ(0, observer.last_tab_removed());
}
namespace {
bool TabViewsInOrder(TabStrip* tab_strip) {
for (int i = 1; i < tab_strip->tab_count(); ++i) {
Tab* left = tab_strip->tab_at(i - 1);
Tab* right = tab_strip->tab_at(i);
if (tab_strip->FindChild(right) < tab_strip->FindChild(left)) {
return false;
}
}
return true;
}
} // namespace
// Verifies child view order matches model order.
TEST_P(TabStripTest, TabViewOrder) {
controller_->AddTab(0, false);
controller_->AddTab(1, false);
controller_->AddTab(2, false);
EXPECT_TRUE(TabViewsInOrder(tab_strip_));
tab_strip_->MoveTab(0, 1, TabRendererData());
EXPECT_TRUE(TabViewsInOrder(tab_strip_));
tab_strip_->MoveTab(1, 2, TabRendererData());
EXPECT_TRUE(TabViewsInOrder(tab_strip_));
tab_strip_->MoveTab(1, 0, TabRendererData());
EXPECT_TRUE(TabViewsInOrder(tab_strip_));
tab_strip_->MoveTab(0, 2, TabRendererData());
EXPECT_TRUE(TabViewsInOrder(tab_strip_));
}
TEST_P(TabStripTest, VisibilityInOverflow) {
constexpr int kInitialWidth = 250;
tab_strip_->SetBounds(0, 0, kInitialWidth, 20);
// The first tab added to a reasonable-width strip should be visible. If we
// add enough additional tabs, eventually one should be invisible due to
// overflow.
int invisible_tab_index = 0;
for (; invisible_tab_index < 100; ++invisible_tab_index) {
controller_->AddTab(invisible_tab_index, false);
if (!tab_strip_->tab_at(invisible_tab_index)->GetVisible())
break;
}
EXPECT_GT(invisible_tab_index, 0);
EXPECT_LT(invisible_tab_index, 100);
// The tabs before the invisible tab should still be visible.
for (int i = 0; i < invisible_tab_index; ++i)
EXPECT_TRUE(tab_strip_->tab_at(i)->GetVisible());
// Enlarging the strip should result in the last tab becoming visible.
tab_strip_->SetBounds(0, 0, kInitialWidth * 2, 20);
EXPECT_TRUE(tab_strip_->tab_at(invisible_tab_index)->GetVisible());
// Shrinking it again should re-hide the last tab.
tab_strip_->SetBounds(0, 0, kInitialWidth, 20);
EXPECT_FALSE(tab_strip_->tab_at(invisible_tab_index)->GetVisible());
// Shrinking it still more should make more tabs invisible, though not all.
// All the invisible tabs should be at the end of the strip.
tab_strip_->SetBounds(0, 0, kInitialWidth / 2, 20);
int i = 0;
for (; i < invisible_tab_index; ++i) {
if (!tab_strip_->tab_at(i)->GetVisible())
break;
}
ASSERT_GT(i, 0);
EXPECT_LT(i, invisible_tab_index);
invisible_tab_index = i;
for (int i = invisible_tab_index + 1; i < tab_strip_->tab_count(); ++i)
EXPECT_FALSE(tab_strip_->tab_at(i)->GetVisible());
// When we're already in overflow, adding tabs at the beginning or end of
// the strip should not change how many tabs are visible.
controller_->AddTab(tab_strip_->tab_count(), false);
EXPECT_TRUE(tab_strip_->tab_at(invisible_tab_index - 1)->GetVisible());
EXPECT_FALSE(tab_strip_->tab_at(invisible_tab_index)->GetVisible());
controller_->AddTab(0, false);
EXPECT_TRUE(tab_strip_->tab_at(invisible_tab_index - 1)->GetVisible());
EXPECT_FALSE(tab_strip_->tab_at(invisible_tab_index)->GetVisible());
// If we remove enough tabs, all the tabs should be visible.
for (int i = tab_strip_->tab_count() - 1; i >= invisible_tab_index; --i)
controller_->RemoveTab(i);
EXPECT_TRUE(tab_strip_->tab_at(tab_strip_->tab_count() - 1)->GetVisible());
}
// Creates a tab strip in stacked layout mode and verifies that as we move
// across the strip at the top, middle, and bottom, events will target each tab
// in order.
TEST_P(TabStripTest, TabForEventWhenStacked) {
tab_strip_->SetBounds(0, 0, 250, GetLayoutConstant(TAB_HEIGHT));
controller_->AddTab(0, false);
controller_->AddTab(1, true);
controller_->AddTab(2, false);
controller_->AddTab(3, false);
ASSERT_EQ(4, tab_strip_->tab_count());
// Switch to stacked layout mode and force a layout to ensure tabs stack.
tab_strip_->SetStackedLayout(true);
CompleteAnimationAndLayout();
gfx::Point p;
for (int y : {0, tab_strip_->height() / 2, tab_strip_->height() - 1}) {
p.set_y(y);
int previous_tab = -1;
for (int x = 0; x < tab_strip_->width(); ++x) {
p.set_x(x);
int tab = tab_strip_->GetModelIndexOfTab(FindTabForEvent(p));
if (tab == previous_tab)
continue;
if ((tab != -1) || (previous_tab != tab_strip_->tab_count() - 1))
EXPECT_EQ(previous_tab + 1, tab) << "p = " << p.ToString();
previous_tab = tab;
}
}
}
// Tests that the tab close buttons of non-active tabs are hidden when
// the tabstrip is in stacked tab mode.
TEST_P(TabStripTest, TabCloseButtonVisibilityWhenStacked) {
// Touch-optimized UI requires a larger width for tabs to show close buttons.
const bool touch_ui = ui::MaterialDesignController::touch_ui();
tab_strip_->SetBounds(0, 0, touch_ui ? 442 : 346, 20);
controller_->AddTab(0, false);
controller_->AddTab(1, true);
controller_->AddTab(2, false);
ASSERT_EQ(3, tab_strip_->tab_count());
Tab* tab0 = tab_strip_->tab_at(0);
Tab* tab1 = tab_strip_->tab_at(1);
ASSERT_TRUE(tab1->IsActive());
Tab* tab2 = tab_strip_->tab_at(2);
// Ensure that all tab close buttons are initially visible.
EXPECT_TRUE(tab0->showing_close_button_);
EXPECT_TRUE(tab1->showing_close_button_);
EXPECT_TRUE(tab2->showing_close_button_);
// Enter stacked layout mode and verify this sets |touch_layout_|.
ASSERT_FALSE(touch_layout());
tab_strip_->SetStackedLayout(true);
ASSERT_TRUE(touch_layout());
// Only the close button of the active tab should be visible in stacked
// layout mode.
EXPECT_FALSE(tab0->showing_close_button_);
EXPECT_TRUE(tab1->showing_close_button_);
EXPECT_FALSE(tab2->showing_close_button_);
// An inactive tab added to the tabstrip should not show
// its tab close button.
controller_->AddTab(3, false);
Tab* tab3 = tab_strip_->tab_at(3);
EXPECT_FALSE(tab0->showing_close_button_);
EXPECT_TRUE(tab1->showing_close_button_);
EXPECT_FALSE(tab2->showing_close_button_);
EXPECT_FALSE(tab3->showing_close_button_);
// After switching tabs, the previously-active tab should have its
// tab close button hidden and the newly-active tab should show
// its tab close button.
tab_strip_->SelectTab(tab2, dummy_event_);
ASSERT_FALSE(tab1->IsActive());
ASSERT_TRUE(tab2->IsActive());
EXPECT_FALSE(tab0->showing_close_button_);
EXPECT_FALSE(tab1->showing_close_button_);
EXPECT_TRUE(tab2->showing_close_button_);
EXPECT_FALSE(tab3->showing_close_button_);
// After closing the active tab, the tab which becomes active should
// show its tab close button.
tab_strip_->CloseTab(tab1, CLOSE_TAB_FROM_TOUCH);
tab1 = nullptr;
ASSERT_TRUE(tab2->IsActive());
EXPECT_FALSE(tab0->showing_close_button_);
EXPECT_TRUE(tab2->showing_close_button_);
EXPECT_FALSE(tab3->showing_close_button_);
// All tab close buttons should be shown when disengaging stacked tab mode.
tab_strip_->SetStackedLayout(false);
ASSERT_FALSE(touch_layout());
EXPECT_TRUE(tab0->showing_close_button_);
EXPECT_TRUE(tab2->showing_close_button_);
EXPECT_TRUE(tab3->showing_close_button_);
}
// Tests that the tab close buttons of non-active tabs are hidden when
// the tabstrip is not in stacked tab mode and the tab sizes are shrunk
// into small sizes.
TEST_P(TabStripTest, TabCloseButtonVisibilityWhenNotStacked) {
// Set the tab strip width to be wide enough for three tabs to show all
// three icons, but not enough for five tabs to show all three icons.
// Touch-optimized UI requires a larger width for tabs to show close buttons.
const bool touch_ui = ui::MaterialDesignController::touch_ui();
tab_strip_->SetBounds(0, 0, touch_ui ? 442 : 346, 20);
controller_->AddTab(0, false);
controller_->AddTab(1, true);
controller_->AddTab(2, false);
ASSERT_EQ(3, tab_strip_->tab_count());
Tab* tab0 = tab_strip_->tab_at(0);
ASSERT_FALSE(tab0->IsActive());
Tab* tab1 = tab_strip_->tab_at(1);
ASSERT_TRUE(tab1->IsActive());
Tab* tab2 = tab_strip_->tab_at(2);
ASSERT_FALSE(tab2->IsActive());
// Ensure this is not in stacked layout mode.
ASSERT_FALSE(touch_layout());
// Ensure that all tab close buttons are initially visible.
EXPECT_TRUE(tab0->showing_close_button_);
EXPECT_TRUE(tab1->showing_close_button_);
EXPECT_TRUE(tab2->showing_close_button_);
// Shrink the tab sizes by adding more tabs.
// An inactive tab added to the tabstrip, now each tab size is not
// big enough to accomodate 3 icons, so it should not show its
// tab close button.
controller_->AddTab(3, false);
Tab* tab3 = tab_strip_->tab_at(3);
EXPECT_FALSE(tab3->showing_close_button_);
// This inactive tab doesn't have alert button, but its favicon and
// title would be shown.
EXPECT_TRUE(tab3->showing_icon_);
EXPECT_FALSE(tab3->showing_alert_indicator_);
EXPECT_TRUE(tab3->title_->GetVisible());
// The active tab's close button still shows.
EXPECT_TRUE(tab1->showing_close_button_);
// An active tab added to the tabstrip should show its tab close
// button.
controller_->AddTab(4, true);
Tab* tab4 = tab_strip_->tab_at(4);
ASSERT_TRUE(tab4->IsActive());
EXPECT_TRUE(tab4->showing_close_button_);
// The previous active button is now inactive so its close
// button should not show.
EXPECT_FALSE(tab1->showing_close_button_);
// After switching tabs, the previously-active tab should have its
// tab close button hidden and the newly-active tab should show
// its tab close button.
tab_strip_->SelectTab(tab2, dummy_event_);
ASSERT_FALSE(tab4->IsActive());
ASSERT_TRUE(tab2->IsActive());
EXPECT_FALSE(tab0->showing_close_button_);
EXPECT_FALSE(tab1->showing_close_button_);
EXPECT_TRUE(tab2->showing_close_button_);
EXPECT_FALSE(tab3->showing_close_button_);
EXPECT_FALSE(tab4->showing_close_button_);
// After closing the active tab, the tab which becomes active should
// show its tab close button.
tab_strip_->CloseTab(tab2, CLOSE_TAB_FROM_TOUCH);
tab2 = nullptr;
ASSERT_TRUE(tab3->IsActive());
CompleteAnimationAndLayout();
EXPECT_FALSE(tab0->showing_close_button_);
EXPECT_FALSE(tab1->showing_close_button_);
EXPECT_TRUE(tab3->showing_close_button_);
EXPECT_FALSE(tab4->showing_close_button_);
}
TEST_P(TabStripTest, GetEventHandlerForOverlappingArea) {
tab_strip_->SetBounds(0, 0, 1000, 20);
controller_->AddTab(0, false);
controller_->AddTab(1, true);
controller_->AddTab(2, false);
controller_->AddTab(3, false);
ASSERT_EQ(4, tab_strip_->tab_count());
// Verify that the active tab will be a tooltip handler for points that hit
// it.
Tab* left_tab = tab_strip_->tab_at(0);
left_tab->SetBoundsRect(gfx::Rect(gfx::Point(0, 0), gfx::Size(200, 20)));
Tab* active_tab = tab_strip_->tab_at(1);
active_tab->SetBoundsRect(gfx::Rect(gfx::Point(150, 0), gfx::Size(200, 20)));
ASSERT_TRUE(active_tab->IsActive());
Tab* right_tab = tab_strip_->tab_at(2);
right_tab->SetBoundsRect(gfx::Rect(gfx::Point(300, 0), gfx::Size(200, 20)));
Tab* most_right_tab = tab_strip_->tab_at(3);
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_strip_->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_strip_->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_strip_->GetEventHandlerForPoint(unactive_overlap)));
}
TEST_P(TabStripTest, GetTooltipHandler) {
tab_strip_->SetBounds(0, 0, 1000, 20);
controller_->AddTab(0, false);
controller_->AddTab(1, true);
controller_->AddTab(2, false);
controller_->AddTab(3, false);
ASSERT_EQ(4, tab_strip_->tab_count());
// Verify that the active tab will be a tooltip handler for points that hit
// it.
Tab* left_tab = tab_strip_->tab_at(0);
left_tab->SetBoundsRect(gfx::Rect(gfx::Point(0, 0), gfx::Size(200, 20)));
Tab* active_tab = tab_strip_->tab_at(1);
active_tab->SetBoundsRect(gfx::Rect(gfx::Point(150, 0), gfx::Size(200, 20)));
ASSERT_TRUE(active_tab->IsActive());
Tab* right_tab = tab_strip_->tab_at(2);
right_tab->SetBoundsRect(gfx::Rect(gfx::Point(300, 0), gfx::Size(200, 20)));
Tab* most_right_tab = tab_strip_->tab_at(3);
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_strip_->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_strip_->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 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_strip_->GetTooltipHandlerForPoint(unactive_overlap)));
// Confirm that tab strip doe not return tooltip handler for points that
// don't hit it.
EXPECT_FALSE(tab_strip_->GetTooltipHandlerForPoint(gfx::Point(-1, 2)));
}
TEST_P(TabStripTest, NewTabButtonStaysVisible) {
const int kTabStripWidth = 500;
tab_strip_->SetBounds(0, 0, kTabStripWidth, 20);
for (int i = 0; i < 100; ++i)
controller_->AddTab(i, (i == 0));
CompleteAnimationAndLayout();
EXPECT_LE(tab_strip_->new_tab_button_bounds().right(), kTabStripWidth);
}
// The cached widths are private, but if they give incorrect results it can
// cause subtle errors in other tests. Therefore it's prudent to test them.
TEST_P(TabStripTest, CachedWidthsReportCorrectSize) {
controller_->AddTab(0, false);
controller_->AddTab(1, true);
controller_->AddTab(2, false);
const int standard_width = TabStyle::GetStandardWidth();
tab_strip_->SetBounds(0, 0, 1000, 100);
EXPECT_EQ(standard_width, GetActiveTabWidth());
EXPECT_EQ(standard_width, GetInactiveTabWidth());
tab_strip_->SetBounds(0, 0, 240, 100);
EXPECT_LT(GetActiveTabWidth(), standard_width);
EXPECT_EQ(GetInactiveTabWidth(), GetActiveTabWidth());
tab_strip_->SetBounds(0, 0, 50, 100);
EXPECT_EQ(TabStyleViews::GetMinimumActiveWidth(), GetActiveTabWidth());
EXPECT_EQ(TabStyleViews::GetMinimumInactiveWidth(), GetInactiveTabWidth());
}
// The active tab should always be at least as wide as its minimum width.
// http://crbug.com/587688
TEST_P(TabStripTest, ActiveTabWidthWhenTabsAreTiny) {
// The bug was caused when it's animating. Therefore we should make widget
// visible so that animation can be triggered.
tab_strip_->GetWidget()->Show();
tab_strip_->SetBounds(0, 0, 200, 20);
// Create a lot of tabs in order to make inactive tabs tiny.
const int min_inactive_width = TabStyleViews::GetMinimumInactiveWidth();
while (GetInactiveTabWidth() != min_inactive_width) {
controller_->CreateNewTab();
CompleteAnimationAndLayout();
}
EXPECT_GT(tab_strip_->tab_count(), 1);
const int active_index = controller_->GetActiveIndex();
EXPECT_EQ(tab_strip_->tab_count() - 1, active_index);
EXPECT_LT(tab_strip_->ideal_bounds(0).width(),
tab_strip_->ideal_bounds(active_index).width());
// During mouse-based tab closure, the active tab should remain at least as
// wide as it's minimum width.
controller_->SelectTab(0, dummy_event_);
while (tab_strip_->tab_count() > 0) {
const int active_index = controller_->GetActiveIndex();
EXPECT_GE(tab_strip_->ideal_bounds(active_index).width(),
TabStyleViews::GetMinimumActiveWidth());
tab_strip_->CloseTab(tab_strip_->tab_at(active_index),
CLOSE_TAB_FROM_MOUSE);
}
}
// Inactive tabs shouldn't shrink during mouse-based tab closure.
// http://crbug.com/850190
TEST_P(TabStripTest, InactiveTabWidthWhenTabsAreTiny) {
tab_strip_->SetBounds(0, 0, 200, 20);
// Create a lot of tabs in order to make inactive tabs smaller than active
// tab but not the minimum.
const int min_inactive_width = TabStyleViews::GetMinimumInactiveWidth();
const int min_active_width = TabStyleViews::GetMinimumActiveWidth();
while (GetInactiveTabWidth() >= (min_inactive_width + min_active_width) / 2) {
controller_->CreateNewTab();
CompleteAnimationAndLayout();
}
// During mouse-based tab closure, inactive tabs shouldn't shrink
// so that users can close tabs continuously without moving mouse.
controller_->SelectTab(0, dummy_event_);
// If there are only two tabs in the strip, then after closing one the
// remaining one will be active and there will be no inactive tabs,
// so we stop at 2.
while (tab_strip_->tab_count() > 2) {
const int last_inactive_width = GetInactiveTabWidth();
tab_strip_->CloseTab(tab_strip_->tab_at(controller_->GetActiveIndex()),
CLOSE_TAB_FROM_MOUSE);
CompleteAnimationAndLayout();
EXPECT_GE(GetInactiveTabWidth(), last_inactive_width);
}
}
// When dragged tabs are moving back to their position, changes to ideal bounds
// should be respected. http://crbug.com/848016
TEST_P(TabStripTest, ResetBoundsForDraggedTabs) {
tab_strip_->SetBounds(0, 0, 200, 20);
// Create a lot of tabs in order to make inactive tabs tiny.
const int min_inactive_width = TabStyleViews::GetMinimumInactiveWidth();
while (GetInactiveTabWidth() != min_inactive_width)
controller_->CreateNewTab();
const int min_active_width = TabStyleViews::GetMinimumActiveWidth();
int dragged_tab_index = controller_->GetActiveIndex();
EXPECT_GE(tab_strip_->ideal_bounds(dragged_tab_index).width(),
min_active_width);
// Mark the active tab as being dragged.
Tab* dragged_tab = tab_strip_->tab_at(dragged_tab_index);
dragged_tab->set_dragging(true);
gfx::AnimationContainerTestApi test_api(bounds_animator()->container());
// Ending the drag triggers the tabstrip to begin animating this tab back
// to its ideal bounds.
StopDraggingTab(dragged_tab);
EXPECT_TRUE(bounds_animator()->IsAnimating(dragged_tab));
// Change the ideal bounds of the tabs mid-animation by selecting a
// different tab.
controller_->SelectTab(0, dummy_event_);
// Once the animation completes, the dragged tab should have animated to
// the new ideal bounds (computed with this as an inactive tab) rather
// than the original ones (where it's an active tab).
const auto duration = base::TimeDelta::FromMilliseconds(
bounds_animator()->GetAnimationDuration());
test_api.IncrementTime(duration);
EXPECT_FALSE(dragged_tab->dragging());
EXPECT_LT(dragged_tab->bounds().width(), min_active_width);
}
// The "blocked" attention indicator should only show for background tabs.
TEST_P(TabStripTest, TabNeedsAttentionBlocked) {
controller_->AddTab(0, false);
controller_->AddTab(1, true);
Tab* tab1 = tab_strip_->tab_at(1);
// Block tab1.
TabRendererData data;
data.blocked = true;
tab1->SetData(data);
EXPECT_FALSE(IsShowingAttentionIndicator(tab1));
controller_->SelectTab(0, dummy_event_);
EXPECT_TRUE(IsShowingAttentionIndicator(tab1));
controller_->SelectTab(1, dummy_event_);
EXPECT_FALSE(IsShowingAttentionIndicator(tab1));
}
// The generic "wants attention" version should always show.
TEST_P(TabStripTest, TabNeedsAttentionGeneric) {
controller_->AddTab(0, false);
controller_->AddTab(1, true);
Tab* tab1 = tab_strip_->tab_at(1);
tab1->SetTabNeedsAttention(true);
EXPECT_TRUE(IsShowingAttentionIndicator(tab1));
controller_->SelectTab(0, dummy_event_);
EXPECT_TRUE(IsShowingAttentionIndicator(tab1));
controller_->SelectTab(1, dummy_event_);
EXPECT_TRUE(IsShowingAttentionIndicator(tab1));
}
TEST_P(TabStripTest, NewTabButtonInkDrop) {
constexpr int kTabStripWidth = 500;
tab_strip_->SetBounds(0, 0, kTabStripWidth, GetLayoutConstant(TAB_HEIGHT));
// Add a few tabs and simulate the new tab button's ink drop animation. This
// should not cause any crashes since the ink drop layer size as well as the
// ink drop container size should remain equal to the new tab button visible
// bounds size. https://crbug.com/814105.
for (int i = 0; i < 10; ++i) {
tab_strip_->new_tab_button()->AnimateInkDropToStateForTesting(
views::InkDropState::ACTION_TRIGGERED);
controller_->AddTab(i, true /* is_active */);
CompleteAnimationAndLayout();
tab_strip_->new_tab_button()->AnimateInkDropToStateForTesting(
views::InkDropState::HIDDEN);
}
}
// Closing tab should be targeted during event dispatching.
TEST_P(TabStripTest, EventsOnClosingTab) {
tab_strip_->SetBounds(0, 0, 200, 20);
controller_->AddTab(0, false);
controller_->AddTab(1, true);
Tab* first_tab = tab_strip_->tab_at(0);
gfx::Point tab_center = first_tab->bounds().CenterPoint();
EXPECT_EQ(first_tab, tab_strip_->GetEventHandlerForPoint(tab_center));
tab_strip_->CloseTab(first_tab, CLOSE_TAB_FROM_MOUSE);
EXPECT_EQ(first_tab, tab_strip_->GetEventHandlerForPoint(tab_center));
}
// Switch selected tabs on horizontal scroll events.
TEST_P(TabStripTest, HorizontalScroll) {
tab_strip_->SetBounds(0, 0, 200, 20);
for (int i = 0; i < 3; i++)
controller_->AddTab(i, true /* is_active */);
Tab* tab = tab_strip_->tab_at(0);
gfx::Point tab_center = tab->bounds().CenterPoint();
for (int i = 0; i < tab_strip_->tab_count(); ++i) {
ui::MouseWheelEvent wheel_event(
gfx::Vector2d(ui::MouseWheelEvent::kWheelDelta, 0), tab_center,
tab_center, ui::EventTimeForNow(), 0, 0);
tab_strip_->OnMouseWheel(wheel_event);
EXPECT_EQ(i, controller_->GetActiveIndex());
}
controller_->SelectTab(0, dummy_event_);
for (int i = tab_strip_->tab_count() - 1; i >= 0; --i) {
ui::MouseWheelEvent wheel_event(
gfx::Vector2d(-ui::MouseWheelEvent::kWheelDelta, 0), tab_center,
tab_center, ui::EventTimeForNow(), 0, 0);
tab_strip_->OnMouseWheel(wheel_event);
EXPECT_EQ(i, controller_->GetActiveIndex());
}
// When offset is smaller than kWheelDelta, we don't scroll immediately.
// We wait offset until accumulated offset gets bigger than kWheelDelta.
const int small_offset = ui::MouseWheelEvent::kWheelDelta / 3;
int next_accumulated_offset = small_offset;
while (next_accumulated_offset < ui::MouseWheelEvent::kWheelDelta) {
ui::MouseWheelEvent wheel_event(gfx::Vector2d(small_offset, 0), tab_center,
tab_center, ui::EventTimeForNow(), 0, 0);
tab_strip_->OnMouseWheel(wheel_event);
EXPECT_EQ(0, controller_->GetActiveIndex());
next_accumulated_offset += small_offset;
}
ui::MouseWheelEvent wheel_event(gfx::Vector2d(small_offset, 0), tab_center,
tab_center, ui::EventTimeForNow(), 0, 0);
tab_strip_->OnMouseWheel(wheel_event);
EXPECT_EQ(1, controller_->GetActiveIndex());
}
TEST_P(TabStripTest, GroupHeaderBasics) {
tab_strip_->SetBounds(0, 0, 1000, 100);
bounds_animator()->SetAnimationDuration(0);
tab_strip_->AddTabAt(0, TabRendererData(), false);
Tab* tab = tab_strip_->tab_at(0);
const int first_slot_x = tab->x();
base::Optional<TabGroupId> group = TabGroupId::GenerateNew();
controller_->MoveTabIntoGroup(0, group);
std::vector<TabGroupHeader*> headers = ListGroupHeaders();
EXPECT_EQ(1u, headers.size());
TabGroupHeader* header = headers[0];
EXPECT_EQ(first_slot_x, header->x());
EXPECT_EQ(tab->width(), header->width());
EXPECT_EQ(tab->height(), header->height());
}
TEST_P(TabStripTest, GroupHeaderBetweenTabs) {
tab_strip_->SetBounds(0, 0, 1000, 100);
bounds_animator()->SetAnimationDuration(0);
tab_strip_->AddTabAt(0, TabRendererData(), false);
tab_strip_->AddTabAt(1, TabRendererData(), false);
const int second_slot_x = tab_strip_->tab_at(1)->x();
base::Optional<TabGroupId> group = TabGroupId::GenerateNew();
controller_->MoveTabIntoGroup(1, group);
TabGroupHeader* header = ListGroupHeaders()[0];
EXPECT_EQ(header->x(), second_slot_x);
}
// This can happen when a tab in the middle of a group starts to close.
TEST_P(TabStripTest, DiscontinuousGroup) {
tab_strip_->SetBounds(0, 0, 1000, 100);
bounds_animator()->SetAnimationDuration(0);
tab_strip_->AddTabAt(0, TabRendererData(), false);
tab_strip_->AddTabAt(1, TabRendererData(), false);
tab_strip_->AddTabAt(2, TabRendererData(), false);
const int first_slot_x = tab_strip_->tab_at(0)->x();
base::Optional<TabGroupId> group = TabGroupId::GenerateNew();
controller_->MoveTabIntoGroup(0, group);
controller_->MoveTabIntoGroup(2, group);
std::vector<TabGroupHeader*> headers = ListGroupHeaders();
EXPECT_EQ(1u, headers.size());
EXPECT_EQ(first_slot_x, headers[0]->x());
}
TEST_P(TabStripTest, DeleteTabGroupHeaderWhenEmpty) {
tab_strip_->AddTabAt(0, TabRendererData(), false);
tab_strip_->AddTabAt(1, TabRendererData(), false);
base::Optional<TabGroupId> group = TabGroupId::GenerateNew();
controller_->MoveTabIntoGroup(0, group);
controller_->MoveTabIntoGroup(1, group);
controller_->MoveTabIntoGroup(0, base::nullopt);
EXPECT_EQ(1u, ListGroupHeaders().size());
controller_->MoveTabIntoGroup(1, base::nullopt);
EXPECT_EQ(0u, ListGroupHeaders().size());
}
INSTANTIATE_TEST_SUITE_P(, TabStripTest, ::testing::Values(false, true));