// 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.h" #include #include "base/macros.h" #include "base/strings/utf_string_conversions.h" #include "chrome/browser/ui/layout_constants.h" #include "chrome/browser/ui/tabs/tab_utils.h" #include "chrome/browser/ui/views/tabs/media_indicator_button.h" #include "chrome/browser/ui/views/tabs/tab_controller.h" #include "grit/theme_resources.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/models/list_selection_model.h" #include "ui/views/controls/button/image_button.h" #include "ui/views/controls/label.h" #include "ui/views/test/views_test_base.h" #include "ui/views/widget/widget.h" using views::Widget; class FakeTabController : public TabController { public: FakeTabController() {} void set_immersive_style(bool value) { immersive_style_ = value; } void set_active_tab(bool value) { active_tab_ = value; } void set_paint_throbber_to_layer(bool value) { paint_throbber_to_layer_ = value; } const ui::ListSelectionModel& GetSelectionModel() const override { return selection_model_; } bool SupportsMultipleSelection() override { return false; } bool ShouldHideCloseButtonForInactiveTabs() override { return false; } void SelectTab(Tab* tab) override {} void ExtendSelectionTo(Tab* tab) override {} void ToggleSelected(Tab* tab) override {} void AddSelectionFromAnchorTo(Tab* tab) override {} void CloseTab(Tab* tab, CloseTabSource source) override {} void ToggleTabAudioMute(Tab* tab) override {} void ShowContextMenuForTab(Tab* tab, const gfx::Point& p, ui::MenuSourceType source_type) override {} bool IsActiveTab(const Tab* tab) const override { return active_tab_; } bool IsTabSelected(const Tab* tab) const override { return false; } bool IsTabPinned(const Tab* tab) const override { return false; } void MaybeStartDrag( Tab* tab, const ui::LocatedEvent& event, const ui::ListSelectionModel& original_selection) override {} void ContinueDrag(views::View* view, const ui::LocatedEvent& event) override { } bool EndDrag(EndDragReason reason) override { return false; } Tab* GetTabAt(Tab* tab, const gfx::Point& tab_in_tab_coordinates) override { return NULL; } void OnMouseEventInTab(views::View* source, const ui::MouseEvent& event) override {} bool ShouldPaintTab(const Tab* tab, gfx::Rect* clip) override { return true; } bool CanPaintThrobberToLayer() const override { return paint_throbber_to_layer_; } bool IsIncognito() const override { return false; } bool IsImmersiveStyle() const override { return immersive_style_; } SkColor GetToolbarTopSeparatorColor() const override { return SK_ColorBLACK; } int GetBackgroundResourceId(bool* custom_image) const override { *custom_image = false; return IDR_THEME_TAB_BACKGROUND; } void UpdateTabAccessibilityState(const Tab* tab, ui::AXViewState* state) override{}; private: ui::ListSelectionModel selection_model_; bool immersive_style_ = false; bool active_tab_ = false; bool paint_throbber_to_layer_ = true; DISALLOW_COPY_AND_ASSIGN(FakeTabController); }; class TabTest : public views::ViewsTestBase { public: TabTest() {} ~TabTest() override {} static views::ImageButton* GetCloseButton(const Tab& tab) { return tab.close_button_; } static views::View* GetThrobberView(const Tab& tab) { // Reinterpret to keep the definition encapsulated (which works so long as // multiple inheritance isn't involved). return reinterpret_cast(tab.throbber_); } static gfx::Rect GetFaviconBounds(const Tab& tab) { return tab.favicon_bounds_; } static int GetTitleWidth(const Tab& tab) { return tab.title_->bounds().width(); } static void LayoutTab(Tab* tab) { tab->Layout(); } static void CheckForExpectedLayoutAndVisibilityOfElements(const Tab& tab) { // Check whether elements are visible when they are supposed to be, given // Tab size and TabRendererData state. if (tab.data_.pinned) { EXPECT_EQ(1, tab.IconCapacity()); if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) { EXPECT_FALSE(tab.ShouldShowIcon()); EXPECT_TRUE(tab.ShouldShowMediaIndicator()); } else { EXPECT_TRUE(tab.ShouldShowIcon()); EXPECT_FALSE(tab.ShouldShowMediaIndicator()); } EXPECT_FALSE(tab.ShouldShowCloseBox()); } else if (tab.IsActive()) { EXPECT_TRUE(tab.ShouldShowCloseBox()); switch (tab.IconCapacity()) { case 0: case 1: EXPECT_FALSE(tab.ShouldShowIcon()); EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; case 2: if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) { EXPECT_FALSE(tab.ShouldShowIcon()); EXPECT_TRUE(tab.ShouldShowMediaIndicator()); } else { EXPECT_TRUE(tab.ShouldShowIcon()); EXPECT_FALSE(tab.ShouldShowMediaIndicator()); } break; default: EXPECT_LE(3, tab.IconCapacity()); EXPECT_TRUE(tab.ShouldShowIcon()); if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) EXPECT_TRUE(tab.ShouldShowMediaIndicator()); else EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; } } else { // Tab not active and not pinned tab. switch (tab.IconCapacity()) { case 0: EXPECT_FALSE(tab.ShouldShowCloseBox()); EXPECT_FALSE(tab.ShouldShowIcon()); EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; case 1: EXPECT_FALSE(tab.ShouldShowCloseBox()); if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) { EXPECT_FALSE(tab.ShouldShowIcon()); EXPECT_TRUE(tab.ShouldShowMediaIndicator()); } else { EXPECT_TRUE(tab.ShouldShowIcon()); EXPECT_FALSE(tab.ShouldShowMediaIndicator()); } break; default: EXPECT_LE(2, tab.IconCapacity()); EXPECT_TRUE(tab.ShouldShowIcon()); if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) EXPECT_TRUE(tab.ShouldShowMediaIndicator()); else EXPECT_FALSE(tab.ShouldShowMediaIndicator()); break; } } // Check positioning of elements with respect to each other, and that they // are fully within the contents bounds. const gfx::Rect contents_bounds = tab.GetContentsBounds(); if (tab.ShouldShowIcon()) { EXPECT_LE(contents_bounds.x(), tab.favicon_bounds_.x()); if (tab.title_->width() > 0) EXPECT_LE(tab.favicon_bounds_.right(), tab.title_->x()); EXPECT_LE(contents_bounds.y(), tab.favicon_bounds_.y()); EXPECT_LE(tab.favicon_bounds_.bottom(), contents_bounds.bottom()); } if (tab.ShouldShowIcon() && tab.ShouldShowMediaIndicator()) EXPECT_LE(tab.favicon_bounds_.right(), GetMediaIndicatorBounds(tab).x()); if (tab.ShouldShowMediaIndicator()) { if (tab.title_->width() > 0) { EXPECT_LE(tab.title_->bounds().right(), GetMediaIndicatorBounds(tab).x()); } EXPECT_LE(GetMediaIndicatorBounds(tab).right(), contents_bounds.right()); EXPECT_LE(contents_bounds.y(), GetMediaIndicatorBounds(tab).y()); EXPECT_LE(GetMediaIndicatorBounds(tab).bottom(), contents_bounds.bottom()); } if (tab.ShouldShowMediaIndicator() && tab.ShouldShowCloseBox()) { // Note: The media indicator can overlap the left-insets of the close box, // but should otherwise be to the left of the close button. EXPECT_LE(GetMediaIndicatorBounds(tab).right(), tab.close_button_->bounds().x() + tab.close_button_->GetInsets().left()); } if (tab.ShouldShowCloseBox()) { // Note: The title bounds can overlap the left-insets of the close box, // but should otherwise be to the left of the close button. if (tab.title_->width() > 0) { EXPECT_LE(tab.title_->bounds().right(), tab.close_button_->bounds().x() + tab.close_button_->GetInsets().left()); } // We need to use the close button contents bounds instead of its bounds, // since it has an empty border around it to extend its clickable area for // touch. // Note: The close button right edge can be outside the nominal contents // bounds, but shouldn't leave the local bounds. const gfx::Rect close_bounds = tab.close_button_->GetContentsBounds(); EXPECT_LE(close_bounds.right(), tab.GetLocalBounds().right()); EXPECT_LE(contents_bounds.y(), close_bounds.y()); EXPECT_LE(close_bounds.bottom(), contents_bounds.bottom()); } } protected: void InitWidget(Widget* widget) { Widget::InitParams params(CreateParams(Widget::InitParams::TYPE_WINDOW)); params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.bounds.SetRect(10, 20, 300, 400); widget->Init(params); } private: static gfx::Rect GetMediaIndicatorBounds(const Tab& tab) { if (!tab.media_indicator_button_) { ADD_FAILURE(); return gfx::Rect(); } return tab.media_indicator_button_->bounds(); } std::string original_locale_; }; TEST_F(TabTest, HitTestTopPixel) { Widget widget; InitWidget(&widget); FakeTabController tab_controller; Tab tab(&tab_controller, nullptr); widget.GetContentsView()->AddChildView(&tab); tab.SetBoundsRect(gfx::Rect(gfx::Point(0, 0), Tab::GetStandardSize())); // Tabs are slanted, so a click halfway down the left edge won't hit it. int middle_y = tab.height() / 2; EXPECT_FALSE(tab.HitTestPoint(gfx::Point(0, middle_y))); // Normally, tabs should not be hit if we click in the exclusion region, only // if we click below it. const int exclusion = GetLayoutConstant(TAB_TOP_EXCLUSION_HEIGHT); int middle_x = tab.width() / 2; EXPECT_FALSE(tab.HitTestPoint(gfx::Point(middle_x, exclusion - 1))); EXPECT_TRUE(tab.HitTestPoint(gfx::Point(middle_x, exclusion))); // If the window is maximized, however, we want clicks in the top edge to // select the tab. widget.Maximize(); EXPECT_TRUE(tab.HitTestPoint(gfx::Point(middle_x, 0))); // But clicks in the area above the slanted sides should still miss. EXPECT_FALSE(tab.HitTestPoint(gfx::Point(0, 0))); EXPECT_FALSE(tab.HitTestPoint(gfx::Point(tab.width() - 1, 0))); } TEST_F(TabTest, LayoutAndVisibilityOfElements) { static const TabMediaState kMediaStatesToTest[] = { TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING, TAB_MEDIA_STATE_AUDIO_PLAYING, TAB_MEDIA_STATE_AUDIO_MUTING }; Widget widget; InitWidget(&widget); FakeTabController controller; Tab tab(&controller, nullptr); widget.GetContentsView()->AddChildView(&tab); SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); TabRendererData data; data.favicon = gfx::ImageSkia::CreateFrom1xBitmap(bitmap); // Perform layout over all possible combinations, checking for correct // results. for (int is_pinned_tab = 0; is_pinned_tab < 2; ++is_pinned_tab) { for (int is_active_tab = 0; is_active_tab < 2; ++is_active_tab) { for (size_t media_state_index = 0; media_state_index < arraysize(kMediaStatesToTest); ++media_state_index) { const TabMediaState media_state = kMediaStatesToTest[media_state_index]; SCOPED_TRACE(::testing::Message() << (is_active_tab ? "Active" : "Inactive") << ' ' << (is_pinned_tab ? "Pinned " : "") << "Tab with media indicator state " << media_state); data.pinned = !!is_pinned_tab; controller.set_active_tab(!!is_active_tab); data.media_state = media_state; tab.SetData(data); // Test layout for every width from standard to minimum. gfx::Rect bounds(gfx::Point(0, 0), Tab::GetStandardSize()); int min_width; if (is_pinned_tab) { bounds.set_width(Tab::GetPinnedWidth()); min_width = Tab::GetPinnedWidth(); } else { min_width = is_active_tab ? Tab::GetMinimumActiveSize().width() : Tab::GetMinimumInactiveSize().width(); } while (bounds.width() >= min_width) { SCOPED_TRACE(::testing::Message() << "bounds=" << bounds.ToString()); tab.SetBoundsRect(bounds); // Invokes Tab::Layout(). CheckForExpectedLayoutAndVisibilityOfElements(tab); bounds.set_width(bounds.width() - 1); } } } } } // Regression test for http://crbug.com/420313: Confirms that any child Views of // Tab do not attempt to provide their own tooltip behavior/text. It also tests // that Tab provides the expected tooltip text (according to tab_utils). TEST_F(TabTest, TooltipProvidedByTab) { Widget widget; InitWidget(&widget); FakeTabController controller; Tab tab(&controller, nullptr); widget.GetContentsView()->AddChildView(&tab); tab.SetBoundsRect(gfx::Rect(Tab::GetStandardSize())); SkBitmap bitmap; bitmap.allocN32Pixels(16, 16); TabRendererData data; data.favicon = gfx::ImageSkia::CreateFrom1xBitmap(bitmap); data.title = base::UTF8ToUTF16( "This is a really long tab title that would case views::Label to provide " "its own tooltip; but Tab should disable that feature so it can provide " "the tooltip instead."); // Test both with and without an indicator showing since the tab tooltip text // should include a description of the media state when the indicator is // present. for (int i = 0; i < 2; ++i) { data.media_state = (i == 0 ? TAB_MEDIA_STATE_NONE : TAB_MEDIA_STATE_AUDIO_PLAYING); SCOPED_TRACE(::testing::Message() << "Tab with media indicator state " << data.media_state); tab.SetData(data); for (int j = 0; j < tab.child_count(); ++j) { views::View& child = *tab.child_at(j); if (!strcmp(child.GetClassName(), "TabCloseButton")) continue; // Close button is excepted. if (!child.visible()) continue; SCOPED_TRACE(::testing::Message() << "child_at(" << j << "): " << child.GetClassName()); const gfx::Point midpoint(child.width() / 2, child.height() / 2); EXPECT_FALSE(child.GetTooltipHandlerForPoint(midpoint)); const gfx::Point mouse_hover_point = midpoint + child.GetMirroredPosition().OffsetFromOrigin(); base::string16 tooltip; EXPECT_TRUE(static_cast(tab).GetTooltipText( mouse_hover_point, &tooltip)); EXPECT_EQ(chrome::AssembleTabTooltipText(data.title, data.media_state), tooltip); } } } // Regression test for http://crbug.com/226253. Calling Layout() more than once // shouldn't change the insets of the close button. TEST_F(TabTest, CloseButtonLayout) { FakeTabController tab_controller; Tab tab(&tab_controller, nullptr); tab.SetBounds(0, 0, 100, 50); LayoutTab(&tab); gfx::Insets close_button_insets = GetCloseButton(tab)->GetInsets(); LayoutTab(&tab); gfx::Insets close_button_insets_2 = GetCloseButton(tab)->GetInsets(); EXPECT_EQ(close_button_insets.top(), close_button_insets_2.top()); EXPECT_EQ(close_button_insets.left(), close_button_insets_2.left()); EXPECT_EQ(close_button_insets.bottom(), close_button_insets_2.bottom()); EXPECT_EQ(close_button_insets.right(), close_button_insets_2.right()); // Also make sure the close button is sized as large as the tab. EXPECT_EQ(50, GetCloseButton(tab)->bounds().height()); } // Tests expected changes to the ThrobberView state when the WebContents loading // state changes or the animation timer (usually in BrowserView) triggers. TEST_F(TabTest, LayeredThrobber) { Widget widget; InitWidget(&widget); FakeTabController tab_controller; Tab tab(&tab_controller, nullptr); widget.GetContentsView()->AddChildView(&tab); tab.SetBoundsRect(gfx::Rect(Tab::GetStandardSize())); views::View* throbber = GetThrobberView(tab); EXPECT_FALSE(throbber->visible()); EXPECT_EQ(TabRendererData::NETWORK_STATE_NONE, tab.data().network_state); EXPECT_EQ(throbber->bounds(), GetFaviconBounds(tab)); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_NONE); EXPECT_FALSE(throbber->visible()); // Simulate a "normal" tab load: should paint to a layer. tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_WAITING); EXPECT_TRUE(tab_controller.CanPaintThrobberToLayer()); EXPECT_TRUE(throbber->visible()); EXPECT_TRUE(throbber->layer()); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_LOADING); EXPECT_TRUE(throbber->visible()); EXPECT_TRUE(throbber->layer()); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_NONE); EXPECT_FALSE(throbber->visible()); // Simulate a drag started and stopped during a load: layer painting stops // temporarily. tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_WAITING); EXPECT_TRUE(throbber->visible()); EXPECT_TRUE(throbber->layer()); tab_controller.set_paint_throbber_to_layer(false); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_WAITING); EXPECT_TRUE(throbber->visible()); EXPECT_FALSE(throbber->layer()); tab_controller.set_paint_throbber_to_layer(true); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_WAITING); EXPECT_TRUE(throbber->visible()); EXPECT_TRUE(throbber->layer()); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_NONE); EXPECT_FALSE(throbber->visible()); // Simulate a tab load starting and stopping during tab dragging (or with // stacked tabs): no layer painting. tab_controller.set_paint_throbber_to_layer(false); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_WAITING); EXPECT_TRUE(throbber->visible()); EXPECT_FALSE(throbber->layer()); tab.UpdateLoadingAnimation(TabRendererData::NETWORK_STATE_NONE); EXPECT_FALSE(throbber->visible()); } TEST_F(TabTest, TitleHiddenWhenSmall) { FakeTabController tab_controller; Tab tab(&tab_controller, nullptr); tab.SetBounds(0, 0, 100, 50); EXPECT_GT(GetTitleWidth(tab), 0); tab.SetBounds(0, 0, 0, 50); EXPECT_EQ(0, GetTitleWidth(tab)); }