| // 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 "base/i18n/rtl.h" |
| #include "base/strings/utf_string_conversions.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 "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() 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 IsImmersiveStyle() const override { return immersive_style_; } |
| 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 ::testing::WithParamInterface<bool> { |
| public: |
| TabTest() {} |
| virtual ~TabTest() {} |
| |
| bool testing_for_rtl_locale() const { return GetParam(); } |
| |
| void SetUp() override { |
| if (testing_for_rtl_locale()) { |
| original_locale_ = base::i18n::GetConfiguredLocale(); |
| base::i18n::SetICUDefaultLocale("he"); |
| } |
| views::ViewsTestBase::SetUp(); |
| } |
| |
| void TearDown() override { |
| views::ViewsTestBase::TearDown(); |
| if (testing_for_rtl_locale()) |
| base::i18n::SetICUDefaultLocale(original_locale_); |
| } |
| |
| 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<views::View*>(tab.throbber_); |
| } |
| |
| static gfx::Rect GetFaviconBounds(const Tab& tab) { |
| return tab.favicon_bounds_; |
| } |
| |
| 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_P(TabTest, HitTestTopPixel) { |
| if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { |
| LOG(WARNING) << "Testing of RTL locale not supported on current platform."; |
| return; |
| } |
| |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SetBoundsRect(gfx::Rect(gfx::Point(0, 0), Tab::GetStandardSize())); |
| |
| // Tabs have some shadow in the top, so by default we don't hit the tab there. |
| int middle_x = tab.width() / 2; |
| EXPECT_FALSE(tab.HitTestPoint(gfx::Point(middle_x, 0))); |
| |
| // 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))); |
| |
| // 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_P(TabTest, LayoutAndVisibilityOfElements) { |
| if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { |
| LOG(WARNING) << "Testing of RTL locale not supported on current platform."; |
| return; |
| } |
| |
| 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); |
| 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_P(TabTest, TooltipProvidedByTab) { |
| if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { |
| LOG(WARNING) << "Testing of RTL locale not supported on current platform."; |
| return; |
| } |
| |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController controller; |
| Tab tab(&controller); |
| 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<views::View&>(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_P(TabTest, CloseButtonLayout) { |
| if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { |
| LOG(WARNING) << "Testing of RTL locale not supported on current platform."; |
| return; |
| } |
| |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller); |
| 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_P(TabTest, LayeredThrobber) { |
| if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { |
| LOG(WARNING) << "Testing of RTL locale not supported on current platform."; |
| return; |
| } |
| |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller); |
| 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 in both a LTR and a RTL locale. Note: The fact that the UI code is |
| // configured for an RTL locale does *not* change how the coordinates are |
| // examined in the tests above because views::View and friends are supposed to |
| // auto-mirror the widgets when painting. Thus, what we're testing here is that |
| // there's no code in Tab that will erroneously subvert this automatic |
| // coordinate translation. http://crbug.com/384179 |
| INSTANTIATE_TEST_CASE_P(, TabTest, ::testing::Values(false, true)); |