| // 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 <stddef.h> |
| |
| #include <utility> |
| |
| #include "base/macros.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/tab_utils.h" |
| #include "chrome/browser/ui/views/tabs/alert_indicator.h" |
| #include "chrome/browser/ui/views/tabs/fake_base_tab_strip_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_close_button.h" |
| #include "chrome/browser/ui/views/tabs/tab_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_icon.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip.h" |
| #include "chrome/browser/ui/views/tabs/tab_style.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/grit/theme_resources.h" |
| #include "chrome/test/views/chrome_views_test_base.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "ui/base/models/list_selection_model.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/favicon_size.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/widget/widget.h" |
| |
| using views::Widget; |
| |
| namespace { |
| bool UsingNewLoadingAnimation() { |
| return base::FeatureList::IsEnabled(features::kNewTabLoadingAnimation); |
| } |
| } // namespace |
| |
| class FakeTabController : public TabController { |
| public: |
| FakeTabController() {} |
| |
| 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; } |
| NewTabButtonPosition GetNewTabButtonPosition() const override { |
| return LEADING; |
| } |
| bool ShouldHideCloseButtonForTab(Tab* tab) const override { return false; } |
| bool MaySetClip() 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 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; } |
| bool IsFirstVisibleTab(const Tab* tab) const override { return false; } |
| bool IsLastVisibleTab(const Tab* tab) const override { return false; } |
| bool SingleTabMode() 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(const gfx::Point& point) override { return nullptr; } |
| const Tab* GetAdjacentTab(const Tab* tab, int offset) override { |
| return nullptr; |
| } |
| void OnMouseEventInTab(views::View* source, |
| const ui::MouseEvent& event) override {} |
| bool ShouldPaintTab(const Tab* tab, float scale, gfx::Path* clip) override { |
| return true; |
| } |
| bool ShouldPaintAsActiveFrame() const override { return true; } |
| int GetStrokeThickness() const override { return 0; } |
| bool CanPaintThrobberToLayer() const override { |
| return paint_throbber_to_layer_; |
| } |
| bool HasVisibleBackgroundTabShapes() const override { return false; } |
| SkColor GetToolbarTopSeparatorColor() const override { return SK_ColorBLACK; } |
| SkColor GetTabSeparatorColor() const override { return SK_ColorBLACK; } |
| SkColor GetTabBackgroundColor( |
| TabState tab_state, |
| BrowserNonClientFrameView::ActiveState active_state = |
| BrowserNonClientFrameView::kUseCurrent) const override { |
| return tab_state == TAB_ACTIVE ? tab_bg_color_active_ |
| : tab_bg_color_inactive_; |
| } |
| SkColor GetTabForegroundColor(TabState tab_state, |
| SkColor background_color) const override { |
| return tab_state == TAB_ACTIVE ? tab_fg_color_active_ |
| : tab_fg_color_inactive_; |
| } |
| int GetBackgroundResourceId( |
| bool* has_custom_image, |
| BrowserNonClientFrameView::ActiveState active_state) const override { |
| *has_custom_image = false; |
| return IDR_THEME_TAB_BACKGROUND; |
| } |
| gfx::Rect GetTabAnimationTargetBounds(const Tab* tab) override { |
| return tab->bounds(); |
| } |
| base::string16 GetAccessibleTabName(const Tab* tab) const override { |
| return base::string16(); |
| } |
| float GetHoverOpacityForTab(float range_parameter) const override { |
| return 1.0f; |
| } |
| float GetHoverOpacityForRadialHighlight() const override { return 1.0f; } |
| |
| void SetTabColors(SkColor bg_color_active, |
| SkColor fg_color_active, |
| SkColor bg_color_inactive, |
| SkColor fg_color_inactive) { |
| tab_bg_color_active_ = bg_color_active; |
| tab_fg_color_active_ = fg_color_active; |
| tab_bg_color_inactive_ = bg_color_inactive; |
| tab_fg_color_inactive_ = fg_color_inactive; |
| } |
| |
| private: |
| ui::ListSelectionModel selection_model_; |
| bool active_tab_ = false; |
| bool paint_throbber_to_layer_ = true; |
| |
| SkColor tab_bg_color_active_ = gfx::kPlaceholderColor; |
| SkColor tab_fg_color_active_ = gfx::kPlaceholderColor; |
| SkColor tab_bg_color_inactive_ = gfx::kPlaceholderColor; |
| SkColor tab_fg_color_inactive_ = gfx::kPlaceholderColor; |
| |
| DISALLOW_COPY_AND_ASSIGN(FakeTabController); |
| }; |
| |
| class TabTest : public ChromeViewsTestBase { |
| public: |
| TabTest() { |
| // Prevent the fake clock from starting at 0 which is the null time. |
| fake_clock_.Advance(base::TimeDelta::FromMilliseconds(2000)); |
| } |
| ~TabTest() override {} |
| |
| static TabIcon* GetTabIcon(const Tab& tab) { return tab.icon_; } |
| |
| static views::Label* GetTabTitle(const Tab& tab) { return tab.title_; } |
| |
| static views::ImageView* GetAlertIndicator(const Tab& tab) { |
| return tab.alert_indicator_; |
| } |
| |
| static views::ImageButton* GetCloseButton(const Tab& tab) { |
| return tab.close_button_; |
| } |
| |
| static int GetTitleWidth(const Tab& tab) { |
| return tab.title_->bounds().width(); |
| } |
| |
| static void EndTitleAnimation(Tab* tab) { tab->title_animation_.End(); } |
| |
| static void LayoutTab(Tab* tab) { tab->Layout(); } |
| |
| static int VisibleIconCount(const Tab& tab) { |
| return tab.showing_icon_ + tab.showing_alert_indicator_ + |
| tab.showing_close_button_; |
| } |
| |
| 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, VisibleIconCount(tab)); |
| if (tab.data_.alert_state != TabAlertState::NONE) { |
| EXPECT_FALSE(tab.showing_icon_); |
| EXPECT_TRUE(tab.showing_alert_indicator_); |
| } else { |
| EXPECT_TRUE(tab.showing_icon_); |
| EXPECT_FALSE(tab.showing_alert_indicator_); |
| } |
| EXPECT_FALSE(tab.title_->visible()); |
| EXPECT_FALSE(tab.showing_close_button_); |
| } else if (tab.IsActive()) { |
| EXPECT_TRUE(tab.showing_close_button_); |
| switch (VisibleIconCount(tab)) { |
| case 1: |
| EXPECT_FALSE(tab.showing_icon_); |
| EXPECT_FALSE(tab.showing_alert_indicator_); |
| break; |
| case 2: |
| if (tab.data_.alert_state != TabAlertState::NONE) { |
| EXPECT_FALSE(tab.showing_icon_); |
| EXPECT_TRUE(tab.showing_alert_indicator_); |
| } else { |
| EXPECT_TRUE(tab.showing_icon_); |
| EXPECT_FALSE(tab.showing_alert_indicator_); |
| } |
| break; |
| default: |
| EXPECT_EQ(3, VisibleIconCount(tab)); |
| EXPECT_TRUE(tab.data_.alert_state != TabAlertState::NONE); |
| break; |
| } |
| } else { // Tab not active and not pinned tab. |
| switch (VisibleIconCount(tab)) { |
| case 1: |
| EXPECT_FALSE(tab.showing_close_button_); |
| if (tab.data_.alert_state == TabAlertState::NONE) { |
| EXPECT_FALSE(tab.showing_alert_indicator_); |
| EXPECT_TRUE(tab.showing_icon_); |
| } else { |
| EXPECT_FALSE(tab.showing_icon_); |
| EXPECT_TRUE(tab.showing_alert_indicator_); |
| } |
| break; |
| case 2: |
| EXPECT_TRUE(tab.showing_icon_); |
| if (tab.data_.alert_state != TabAlertState::NONE) |
| EXPECT_TRUE(tab.showing_alert_indicator_); |
| else |
| EXPECT_FALSE(tab.showing_alert_indicator_); |
| break; |
| default: |
| EXPECT_EQ(3, VisibleIconCount(tab)); |
| EXPECT_TRUE(tab.data_.alert_state != TabAlertState::NONE); |
| } |
| } |
| |
| // 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.showing_icon_) { |
| if (tab.center_icon_) { |
| EXPECT_LE(tab.icon_->x(), contents_bounds.x()); |
| } else { |
| EXPECT_LE(contents_bounds.x(), tab.icon_->x()); |
| } |
| if (tab.title_->visible()) |
| EXPECT_LE(tab.icon_->bounds().right(), tab.title_->x()); |
| EXPECT_LE(contents_bounds.y(), tab.icon_->y()); |
| EXPECT_LE(tab.icon_->bounds().bottom(), contents_bounds.bottom()); |
| } |
| |
| if (tab.showing_icon_ && tab.showing_alert_indicator_) { |
| // When checking for overlap, other views should not overlap the main |
| // favicon (covered by kFaviconSize) but can overlap the extra space |
| // reserved for the attention indicator. |
| int icon_visual_right = tab.icon_->bounds().x() + gfx::kFaviconSize; |
| EXPECT_LE(icon_visual_right, GetAlertIndicatorBounds(tab).x()); |
| } |
| |
| if (tab.showing_alert_indicator_) { |
| if (tab.title_->visible()) { |
| EXPECT_LE(tab.title_->bounds().right(), |
| GetAlertIndicatorBounds(tab).x()); |
| } |
| if (tab.center_icon_) { |
| EXPECT_LE(contents_bounds.right(), |
| GetAlertIndicatorBounds(tab).right()); |
| } else { |
| EXPECT_LE(GetAlertIndicatorBounds(tab).right(), |
| contents_bounds.right()); |
| } |
| EXPECT_LE(contents_bounds.y(), GetAlertIndicatorBounds(tab).y()); |
| EXPECT_LE(GetAlertIndicatorBounds(tab).bottom(), |
| contents_bounds.bottom()); |
| } |
| if (tab.showing_alert_indicator_ && tab.showing_close_button_) { |
| // Note: The alert indicator can overlap the left-insets of the close box, |
| // but should otherwise be to the left of the close button. |
| EXPECT_LE(GetAlertIndicatorBounds(tab).right(), |
| tab.close_button_->bounds().x() + |
| tab.close_button_->GetInsets().left()); |
| } |
| if (tab.showing_close_button_) { |
| // 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_->visible()) { |
| 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()); |
| } |
| } |
| |
| static void StopFadeAnimationIfNecessary(const Tab& tab) { |
| // Stop the fade animation directly instead of waiting an unknown number of |
| // seconds. |
| gfx::Animation* fade_animation = |
| tab.alert_indicator_->fade_animation_.get(); |
| if (fade_animation) |
| fade_animation->Stop(); |
| } |
| |
| void SetupFakeClock(TabIcon* icon) { icon->clock_ = &fake_clock_; } |
| |
| void FinishRunningLoadingAnimations(TabIcon* icon) { |
| // Forward the clock enough for any running animations to finish. |
| DCHECK(icon->clock_ == &fake_clock_); |
| constexpr base::TimeDelta delta = base::TimeDelta::FromMilliseconds(2000); |
| fake_clock_.Advance(delta); |
| icon->StepLoadingAnimation(icon->waiting_state_.elapsed_time + delta); |
| icon->animation_state_ = icon->pending_animation_state_; |
| } |
| |
| static float GetLoadingProgress(TabIcon* icon) { |
| return icon->target_loading_progress_; |
| } |
| |
| 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 GetAlertIndicatorBounds(const Tab& tab) { |
| if (!tab.alert_indicator_) { |
| ADD_FAILURE(); |
| return gfx::Rect(); |
| } |
| return tab.alert_indicator_->bounds(); |
| } |
| |
| std::string original_locale_; |
| base::SimpleTestTickClock fake_clock_; |
| }; |
| |
| class AlertIndicatorTest : public ChromeViewsTestBase { |
| public: |
| AlertIndicatorTest() {} |
| ~AlertIndicatorTest() override {} |
| |
| void SetUp() override { |
| ChromeViewsTestBase::SetUp(); |
| |
| controller_ = new FakeBaseTabStripController; |
| tab_strip_ = new TabStrip(std::unique_ptr<TabStripController>(controller_)); |
| controller_->set_tab_strip(tab_strip_); |
| // The tab strip must be added to the view hierarchy for it to create the |
| // buttons. |
| parent_.AddChildView(tab_strip_); |
| parent_.set_owned_by_client(); |
| |
| 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, 400, 400); |
| widget_->Init(init_params); |
| widget_->SetContentsView(&parent_); |
| } |
| |
| void TearDown() override { |
| // All windows need to be closed before tear down. |
| widget_.reset(); |
| |
| ChromeViewsTestBase::TearDown(); |
| } |
| |
| protected: |
| bool showing_close_button(Tab* tab) const { |
| return tab->showing_close_button_; |
| } |
| bool showing_icon(Tab* tab) const { return tab->showing_icon_; } |
| bool showing_alert_indicator(Tab* tab) const { |
| return tab->showing_alert_indicator_; |
| } |
| |
| void StopAnimation(Tab* tab) { |
| ASSERT_TRUE(tab->alert_indicator_->fade_animation_); |
| tab->alert_indicator_->fade_animation_->Stop(); |
| } |
| |
| // Owned by TabStrip. |
| FakeBaseTabStripController* controller_ = nullptr; |
| // Owns |tab_strip_|. |
| views::View parent_; |
| TabStrip* tab_strip_ = nullptr; |
| std::unique_ptr<views::Widget> widget_; |
| |
| private: |
| DISALLOW_COPY_AND_ASSIGN(AlertIndicatorTest); |
| }; |
| |
| TEST_F(TabTest, HitTestTopPixel) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SizeToPreferredSize(); |
| |
| // 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))); |
| |
| // Tabs should not be hit if we click above them. |
| int middle_x = tab.width() / 2; |
| EXPECT_FALSE(tab.HitTestPoint(gfx::Point(middle_x, -1))); |
| EXPECT_TRUE(tab.HitTestPoint(gfx::Point(middle_x, 0))); |
| |
| // Make sure top edge clicks still select the tab when the window is |
| // maximized. |
| 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 TabAlertState kAlertStatesToTest[] = { |
| TabAlertState::NONE, TabAlertState::TAB_CAPTURING, |
| TabAlertState::AUDIO_PLAYING, TabAlertState::AUDIO_MUTING, |
| TabAlertState::PIP_PLAYING, |
| }; |
| |
| 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 (bool is_pinned_tab : {false, true}) { |
| for (bool is_active_tab : {false, true}) { |
| for (TabAlertState alert_state : kAlertStatesToTest) { |
| SCOPED_TRACE(::testing::Message() |
| << (is_active_tab ? "Active " : "Inactive ") |
| << (is_pinned_tab ? "pinned " : "") |
| << "tab with alert indicator state " |
| << static_cast<int>(alert_state)); |
| |
| data.pinned = is_pinned_tab; |
| controller.set_active_tab(is_active_tab); |
| data.alert_state = alert_state; |
| tab.SetData(data); |
| StopFadeAnimationIfNecessary(tab); |
| |
| // Test layout for every width from standard to minimum. |
| int width, min_width; |
| if (is_pinned_tab) { |
| width = min_width = TabStyle::GetPinnedWidth(); |
| } else { |
| width = TabStyle::GetStandardWidth(); |
| min_width = is_active_tab ? TabStyle::GetMinimumActiveWidth() |
| : TabStyle::GetMinimumInactiveWidth(); |
| } |
| const int height = GetLayoutConstant(TAB_HEIGHT); |
| for (; width >= min_width; --width) { |
| SCOPED_TRACE(::testing::Message() << "width=" << width); |
| tab.SetBounds(0, 0, width, height); // Invokes Tab::Layout(). |
| CheckForExpectedLayoutAndVisibilityOfElements(tab); |
| } |
| } |
| } |
| } |
| } |
| |
| // Regression test for http://crbug.com/420313: Confirms that any child Views of |
| // Tab do not attempt to provide their own tooltip behavior/text. |
| TEST_F(TabTest, TooltipProvidedByTab) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController controller; |
| Tab tab(&controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SizeToPreferredSize(); |
| |
| 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 alert state when the indicator is |
| // present. |
| for (int i = 0; i < 2; ++i) { |
| data.alert_state = |
| (i == 0 ? TabAlertState::NONE : TabAlertState::AUDIO_PLAYING); |
| SCOPED_TRACE(::testing::Message() << "Tab with alert indicator state " |
| << static_cast<int>(data.alert_state)); |
| tab.SetData(data); |
| const base::string16 expected_tooltip = |
| Tab::GetTooltipText(data.title, data.alert_state); |
| |
| 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(tab.GetTooltipText(mouse_hover_point, &tooltip)); |
| EXPECT_EQ(expected_tooltip, 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()); |
| } |
| |
| // Regression test for http://crbug.com/609701. Ensure TabCloseButton does not |
| // get focus on right click. |
| TEST_F(TabTest, CloseButtonFocus) { |
| Widget widget; |
| InitWidget(&widget); |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| |
| views::ImageButton* tab_close_button = GetCloseButton(tab); |
| |
| // Verify tab_close_button does not get focus on right click. |
| ui::MouseEvent right_click_event(ui::ET_KEY_PRESSED, gfx::Point(), |
| gfx::Point(), base::TimeTicks(), |
| ui::EF_RIGHT_MOUSE_BUTTON, 0); |
| tab_close_button->OnMousePressed(right_click_event); |
| EXPECT_NE(tab_close_button, |
| tab_close_button->GetFocusManager()->GetFocusedView()); |
| } |
| |
| // 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.SizeToPreferredSize(); |
| |
| TabIcon* icon = GetTabIcon(tab); |
| SetupFakeClock(icon); |
| TabRendererData data; |
| data.url = GURL("http://example.com"); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| EXPECT_EQ(TabNetworkState::kNone, tab.data().network_state); |
| |
| // Simulate a "normal" tab load: should paint to a layer. |
| data.network_state = TabNetworkState::kWaiting; |
| tab.SetData(data); |
| EXPECT_TRUE(tab_controller.CanPaintThrobberToLayer()); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kLoading; |
| tab.SetData(data); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab.SetData(data); |
| if (UsingNewLoadingAnimation()) { |
| // The post-loading animation should still be playing (loading bar fades |
| // out). |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| FinishRunningLoadingAnimations(icon); |
| } |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| |
| // Simulate a tab that should hide throbber. |
| data.should_hide_throbber = true; |
| tab.SetData(data); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| data.network_state = TabNetworkState::kWaiting; |
| tab.SetData(data); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| data.network_state = TabNetworkState::kLoading; |
| tab.SetData(data); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| data.network_state = TabNetworkState::kNone; |
| tab.SetData(data); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| |
| // Simulate a tab that should not hide throbber. |
| data.should_hide_throbber = false; |
| data.network_state = TabNetworkState::kWaiting; |
| tab.SetData(data); |
| EXPECT_TRUE(tab_controller.CanPaintThrobberToLayer()); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kLoading; |
| tab.SetData(data); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab.SetData(data); |
| if (UsingNewLoadingAnimation()) { |
| // The post-loading animation should still be playing (loading bar fades |
| // out). |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| FinishRunningLoadingAnimations(icon); |
| } |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| |
| // After loading is done, simulate another resource starting to load. |
| data.network_state = TabNetworkState::kWaiting; |
| tab.SetData(data); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| |
| // Reset. |
| data.network_state = TabNetworkState::kNone; |
| tab.SetData(data); |
| FinishRunningLoadingAnimations(icon); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| |
| // Simulate a drag started and stopped during a load: layer painting stops |
| // temporarily. |
| data.network_state = TabNetworkState::kWaiting; |
| tab.SetData(data); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| tab_controller.set_paint_throbber_to_layer(false); |
| tab.StepLoadingAnimation(base::TimeDelta::FromMilliseconds(100)); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_FALSE(icon->layer()); |
| tab_controller.set_paint_throbber_to_layer(true); |
| tab.StepLoadingAnimation(base::TimeDelta::FromMilliseconds(100)); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab.SetData(data); |
| FinishRunningLoadingAnimations(icon); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| |
| // 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); |
| data.network_state = TabNetworkState::kWaiting; |
| tab.SetData(data); |
| EXPECT_TRUE(icon->ShowingLoadingAnimation()); |
| EXPECT_FALSE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab.SetData(data); |
| FinishRunningLoadingAnimations(icon); |
| EXPECT_FALSE(icon->ShowingLoadingAnimation()); |
| } |
| |
| // This is enforced as the loading progress is used for painting the progress |
| // bar. When the progress bar is done loading and is fading out we want it to be |
| // painted to the full width. |
| TEST_F(TabTest, LoadingProgressIsFixedTo100PercentWhenNotLoading) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SizeToPreferredSize(); |
| |
| TabIcon* icon = GetTabIcon(tab); |
| TabRendererData data; |
| data.url = GURL("http://example.com"); |
| data.network_state = TabNetworkState::kWaiting; |
| EXPECT_FLOAT_EQ(1.0, GetLoadingProgress(icon)); |
| data.load_progress = 0.2; |
| tab.SetData(data); |
| EXPECT_FLOAT_EQ(1.0, GetLoadingProgress(icon)); |
| } |
| |
| TEST_F(TabTest, LoadingProgressMonotonicallyIncreases) { |
| if (!UsingNewLoadingAnimation()) |
| return; |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SizeToPreferredSize(); |
| |
| TabIcon* icon = GetTabIcon(tab); |
| TabRendererData data; |
| data.network_state = TabNetworkState::kLoading; |
| data.load_progress = 0.2; |
| tab.SetData(data); |
| float initial_reported_progress = GetLoadingProgress(icon); |
| // Reported progress should interpolate to something between itself and 1.0. |
| EXPECT_GE(initial_reported_progress, 0.2); |
| EXPECT_LT(initial_reported_progress, 1.0); |
| |
| // Decrease load progress, icon's load progress should not change. |
| data.load_progress = 0.1; |
| tab.SetData(data); |
| EXPECT_FLOAT_EQ(initial_reported_progress, GetLoadingProgress(icon)); |
| |
| // Though increasing it should be respected. |
| data.load_progress = 0.5; |
| tab.SetData(data); |
| // A higher load progress should be interpolate to larger value (less than 1). |
| EXPECT_GT(GetLoadingProgress(icon), initial_reported_progress); |
| EXPECT_LT(GetLoadingProgress(icon), 1.0); |
| } |
| |
| TEST_F(TabTest, LoadingProgressGoesTo100PercentAfterLoadingIsDone) { |
| if (!UsingNewLoadingAnimation()) |
| return; |
| |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController tab_controller; |
| Tab tab(&tab_controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SizeToPreferredSize(); |
| |
| TabIcon* icon = GetTabIcon(tab); |
| TabRendererData data; |
| data.network_state = TabNetworkState::kLoading; |
| data.load_progress = 0.2; |
| tab.SetData(data); |
| // Reported progress should interpolate to something between itself and 1.0. |
| EXPECT_GE(GetLoadingProgress(icon), 0.2); |
| EXPECT_LT(GetLoadingProgress(icon), 1.0); |
| |
| // Finish loading. Regardless of reported |data.load_progress|, load_progress |
| // should be drawn at 100%. |
| data.network_state = TabNetworkState::kNone; |
| tab.SetData(data); |
| EXPECT_FLOAT_EQ(1.0, GetLoadingProgress(icon)); |
| } |
| |
| 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)); |
| } |
| |
| TEST_F(TabTest, FaviconDoesntMoveWhenShowingAlertIndicator) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| for (bool is_active_tab : {false, true}) { |
| FakeTabController controller; |
| controller.set_active_tab(is_active_tab); |
| Tab tab(&controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SizeToPreferredSize(); |
| |
| views::View* icon = GetTabIcon(tab); |
| int icon_x = icon->x(); |
| TabRendererData data; |
| data.alert_state = TabAlertState::AUDIO_PLAYING; |
| tab.SetData(data); |
| EXPECT_EQ(icon_x, icon->x()); |
| } |
| } |
| |
| TEST_F(TabTest, SmallTabsHideCloseButton) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController controller; |
| controller.set_active_tab(false); |
| Tab tab(&controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| const int width = tab.tab_style()->GetContentsInsets().width() + |
| Tab::kMinimumContentsWidthForCloseButtons; |
| tab.SetBounds(0, 0, width, 50); |
| const views::View* close = GetCloseButton(tab); |
| EXPECT_TRUE(close->visible()); |
| |
| const views::View* icon = GetTabIcon(tab); |
| const int icon_x = icon->x(); |
| // Shrink the tab. The close button should disappear. |
| tab.SetBounds(0, 0, width - 1, 50); |
| EXPECT_FALSE(close->visible()); |
| // The favicon moves left because the extra padding disappears too. |
| EXPECT_LT(icon->x(), icon_x); |
| } |
| |
| TEST_F(TabTest, ExtraLeftPaddingNotShownOnSmallActiveTab) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController controller; |
| controller.set_active_tab(true); |
| Tab tab(&controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| tab.SetBounds(0, 0, 200, 50); |
| const views::View* close = GetCloseButton(tab); |
| EXPECT_TRUE(close->visible()); |
| |
| const views::View* icon = GetTabIcon(tab); |
| const int icon_x = icon->x(); |
| |
| tab.SetBounds(0, 0, 40, 50); |
| EXPECT_TRUE(close->visible()); |
| // The favicon moves left because the extra padding disappears. |
| EXPECT_LT(icon->x(), icon_x); |
| } |
| |
| TEST_F(TabTest, ExtraLeftPaddingShownOnSiteWithoutFavicon) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController controller; |
| Tab tab(&controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| |
| tab.SizeToPreferredSize(); |
| const views::View* icon = GetTabIcon(tab); |
| const int icon_x = icon->x(); |
| |
| // Remove the favicon. |
| TabRendererData data; |
| data.show_icon = false; |
| tab.SetData(data); |
| EndTitleAnimation(&tab); |
| EXPECT_FALSE(icon->visible()); |
| // Title should be placed where the favicon was. |
| EXPECT_EQ(icon_x, GetTabTitle(tab)->x()); |
| } |
| |
| TEST_F(TabTest, ExtraAlertPaddingNotShownOnSmallActiveTab) { |
| Widget widget; |
| InitWidget(&widget); |
| |
| FakeTabController controller; |
| controller.set_active_tab(true); |
| Tab tab(&controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| TabRendererData data; |
| data.alert_state = TabAlertState::AUDIO_PLAYING; |
| tab.SetData(data); |
| |
| tab.SetBounds(0, 0, 200, 50); |
| EXPECT_TRUE(GetTabIcon(tab)->visible()); |
| const views::View* close = GetCloseButton(tab); |
| const views::View* alert = GetAlertIndicator(tab); |
| const int original_spacing = close->x() - alert->bounds().right(); |
| |
| tab.SetBounds(0, 0, 70, 50); |
| EXPECT_FALSE(GetTabIcon(tab)->visible()); |
| EXPECT_TRUE(close->visible()); |
| EXPECT_TRUE(alert->visible()); |
| // The alert indicator moves closer because the extra padding is gone. |
| EXPECT_LT(close->x() - alert->bounds().right(), original_spacing); |
| } |
| |
| TEST_F(TabTest, TitleTextHasSufficientContrast) { |
| constexpr SkColor kDarkGray = SkColorSetRGB(0x22, 0x22, 0x22); |
| constexpr SkColor kLightGray = SkColorSetRGB(0x99, 0x99, 0x99); |
| struct ColorScheme { |
| SkColor bg_active; |
| SkColor fg_active; |
| SkColor bg_inactive; |
| SkColor fg_inactive; |
| } color_schemes[] = { |
| { |
| SK_ColorBLACK, SK_ColorWHITE, SK_ColorBLACK, SK_ColorWHITE, |
| }, |
| { |
| SK_ColorBLACK, SK_ColorWHITE, SK_ColorWHITE, SK_ColorBLACK, |
| }, |
| { |
| kDarkGray, kLightGray, kDarkGray, kLightGray, |
| }, |
| }; |
| |
| // Create a tab inside a Widget, so it has a theme provider, so the call to |
| // UpdateForegroundColors() below doesn't no-op. |
| Widget widget; |
| InitWidget(&widget); |
| FakeTabController controller; |
| Tab tab(&controller, nullptr); |
| widget.GetContentsView()->AddChildView(&tab); |
| |
| for (const auto& colors : color_schemes) { |
| controller.SetTabColors(colors.bg_active, colors.fg_active, |
| colors.bg_inactive, colors.fg_inactive); |
| for (TabState state : {TAB_INACTIVE, TAB_ACTIVE}) { |
| controller.set_active_tab(state == TAB_ACTIVE); |
| tab.UpdateForegroundColors(); |
| const SkColor fg_color = tab.title_->enabled_color(); |
| const SkColor bg_color = controller.GetTabBackgroundColor(state); |
| const float contrast = color_utils::GetContrastRatio(fg_color, bg_color); |
| EXPECT_GE(contrast, color_utils::kMinimumReadableContrastRatio); |
| } |
| } |
| } |
| |
| // This test verifies that the tab has its icon state updated when the alert |
| // animation fade-out finishes. |
| TEST_F(AlertIndicatorTest, ShowsAndHidesAlertIndicator) { |
| controller_->AddPinnedTab(0, false); |
| controller_->AddTab(1, true); |
| Tab* media_tab = tab_strip_->tab_at(0); |
| |
| // Pinned inactive tab only has an icon. |
| EXPECT_TRUE(showing_icon(media_tab)); |
| EXPECT_FALSE(showing_alert_indicator(media_tab)); |
| EXPECT_FALSE(showing_close_button(media_tab)); |
| |
| TabRendererData start_media; |
| start_media.alert_state = TabAlertState::AUDIO_PLAYING; |
| start_media.pinned = media_tab->data().pinned; |
| media_tab->SetData(std::move(start_media)); |
| |
| // When audio starts, pinned inactive tab shows indicator. |
| EXPECT_FALSE(showing_icon(media_tab)); |
| EXPECT_TRUE(showing_alert_indicator(media_tab)); |
| EXPECT_FALSE(showing_close_button(media_tab)); |
| |
| TabRendererData stop_media; |
| stop_media.alert_state = TabAlertState::NONE; |
| stop_media.pinned = media_tab->data().pinned; |
| media_tab->SetData(std::move(stop_media)); |
| |
| // When audio ends, pinned inactive tab fades out indicator. |
| EXPECT_FALSE(showing_icon(media_tab)); |
| EXPECT_TRUE(showing_alert_indicator(media_tab)); |
| EXPECT_FALSE(showing_close_button(media_tab)); |
| |
| // Rather than flakily waiting some unknown number of seconds for the fade |
| // out animation to stop, reach out and stop the fade animation directly, |
| // to make sure that it updates the tab appropriately when it's done. |
| StopAnimation(media_tab); |
| |
| EXPECT_TRUE(showing_icon(media_tab)); |
| EXPECT_FALSE(showing_alert_indicator(media_tab)); |
| EXPECT_FALSE(showing_close_button(media_tab)); |
| } |