| // Copyright 2012 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/views/tabs/tab.h" |
| |
| #include <stddef.h> |
| |
| #include <memory> |
| #include <utility> |
| |
| #include "base/memory/raw_ptr.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/test/scoped_feature_list.h" |
| #include "base/test/simple_test_tick_clock.h" |
| #include "chrome/browser/ui/browser_window/test/mock_browser_window_interface.h" |
| #include "chrome/browser/ui/layout_constants.h" |
| #include "chrome/browser/ui/tabs/alert/tab_alert.h" |
| #include "chrome/browser/ui/tabs/saved_tab_groups/collaboration_messaging_tab_data.h" |
| #include "chrome/browser/ui/tabs/tab_style.h" |
| #include "chrome/browser/ui/tabs/tab_types.h" |
| #include "chrome/browser/ui/views/tabs/alert_indicator_button.h" |
| #include "chrome/browser/ui/views/tabs/fake_base_tab_strip_controller.h" |
| #include "chrome/browser/ui/views/tabs/fake_tab_slot_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_close_button.h" |
| #include "chrome/browser/ui/views/tabs/tab_icon.h" |
| #include "chrome/browser/ui/views/tabs/tab_slot_controller.h" |
| #include "chrome/browser/ui/views/tabs/tab_slot_view.h" |
| #include "chrome/browser/ui/views/tabs/tab_strip.h" |
| #include "chrome/browser/ui/views/tabs/tab_style_views.h" |
| #include "chrome/common/chrome_features.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "chrome/test/views/chrome_views_test_base.h" |
| #include "components/collaboration/public/messaging/message.h" |
| #include "components/content_settings/core/common/features.h" |
| #include "components/tab_groups/tab_group_id.h" |
| #include "components/tab_groups/tab_group_visual_data.h" |
| #include "components/tabs/public/mock_tab_interface.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/base/models/list_selection_model.h" |
| #include "ui/base/unowned_user_data/unowned_user_data_host.h" |
| #include "ui/gfx/color_palette.h" |
| #include "ui/gfx/color_utils.h" |
| #include "ui/gfx/favicon_size.h" |
| #include "ui/views/accessibility/view_accessibility.h" |
| #include "ui/views/controls/button/image_button.h" |
| #include "ui/views/controls/label.h" |
| #include "ui/views/layout/flex_layout.h" |
| #include "ui/views/test/views_test_utils.h" |
| #include "ui/views/view_class_properties.h" |
| #include "ui/views/widget/widget.h" |
| |
| using views::Widget; |
| |
| namespace { |
| |
| using collaboration::messaging::CollaborationEvent; |
| using collaboration::messaging::PersistentMessage; |
| using collaboration::messaging::PersistentNotificationType; |
| |
| PersistentMessage CreateMessage(std::string given_name, |
| CollaborationEvent event) { |
| data_sharing::GroupMember user; |
| user.given_name = given_name; |
| |
| collaboration::messaging::MessageAttribution attr; |
| attr.triggering_user = user; |
| |
| collaboration::messaging::PersistentMessage message; |
| message.collaboration_event = event; |
| message.attribution = attr; |
| message.type = PersistentNotificationType::CHIP; |
| |
| return message; |
| } |
| |
| } // namespace |
| |
| class TabTest : public ChromeViewsTestBase { |
| public: |
| TabTest() { |
| // Prevent the fake clock from starting at 0 which is the null time. |
| fake_clock_.Advance(base::Milliseconds(2000)); |
| } |
| ~TabTest() override = default; |
| |
| static TabIcon* GetTabIcon(Tab* tab) { return tab->icon_; } |
| |
| static views::Label* GetTabTitle(Tab* tab) { return tab->title_; } |
| |
| static views::ImageButton* GetAlertIndicator(Tab* tab) { |
| return tab->alert_indicator_button_; |
| } |
| |
| static TabCloseButton* GetCloseButton(Tab* tab) { return tab->close_button_; } |
| |
| static int GetTitleWidth(Tab* tab) { return tab->title_->bounds().width(); } |
| |
| static void EndTitleAnimation(Tab* tab) { tab->title_animation_.End(); } |
| |
| static void LayoutTab(Tab* tab) { views::test::RunScheduledLayout(tab); } |
| |
| 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.size()) { |
| 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_->GetVisible()); |
| 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.size()) { |
| 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_FALSE(tab.data_.alert_state.empty()); |
| 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.empty()) { |
| 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.size()) { |
| EXPECT_TRUE(tab.showing_alert_indicator_); |
| } else { |
| EXPECT_FALSE(tab.showing_alert_indicator_); |
| } |
| break; |
| default: |
| EXPECT_EQ(3, VisibleIconCount(tab)); |
| EXPECT_FALSE(tab.data_.alert_state.empty()); |
| } |
| } |
| |
| // Check the tab icon's positioning. Icons should be positioned at the |
| // start of the tab. Favicons should be centered within their icons. We |
| // extend the bounds vertically down along the tab so that the crashed tabs |
| // and alerts icons can be placed. This means that the true bounds are not |
| // centered on the contents bounds. |
| const gfx::Rect contents_bounds = tab.GetContentsBounds(); |
| if (tab.showing_icon_) { |
| gfx::Rect icon_bounds = tab.icon_->bounds(); |
| icon_bounds.Inset(tab.icon_->GetInsets()); |
| if (tab.center_icon_) { |
| EXPECT_LE(icon_bounds.x(), contents_bounds.x()); |
| } else { |
| EXPECT_LE(contents_bounds.x(), icon_bounds.x()); |
| } |
| if (tab.title_->GetVisible()) { |
| EXPECT_LE(tab.icon_->bounds().right(), tab.title_->x()); |
| } |
| |
| // Tab Icon content now exactly fit the content bounds. |
| EXPECT_EQ(icon_bounds.y(), contents_bounds.y()); |
| EXPECT_GE(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_->GetVisible()) { |
| 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()); |
| } |
| |
| // The alert indicator should be centered in the content bounds. |
| gfx::Rect alert_bounds = GetAlertIndicatorBounds(tab); |
| EXPECT_EQ(alert_bounds.CenterPoint().y(), |
| contents_bounds.CenterPoint().y()); |
| } |
| |
| 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_->GetVisible()) { |
| EXPECT_LE(tab.title_->bounds().right(), |
| tab.close_button_->bounds().x() + |
| tab.close_button_->GetInsets().left()); |
| } |
| |
| // The close button has a larger hit target than the content bounds. |
| const gfx::Rect close_bounds = tab.close_button_->GetContentsBounds(); |
| EXPECT_LE(close_bounds.right(), tab.GetLocalBounds().right()); |
| EXPECT_LE(close_bounds.y(), contents_bounds.y()); |
| EXPECT_LE(contents_bounds.bottom(), close_bounds.bottom()); |
| } |
| } |
| |
| static void StopFadeAnimationIfNecessary(const Tab& tab) { |
| // Stop the fade animation directly instead of waiting an unknown number of |
| // seconds. |
| if (gfx::Animation* fade_animation = |
| tab.alert_indicator_button_->fade_animation_.get()) { |
| fade_animation->Stop(); |
| } |
| } |
| |
| void SetupFakeClock(TabIcon* icon) { icon->clock_ = &fake_clock_; } |
| |
| private: |
| static gfx::Rect GetAlertIndicatorBounds(const Tab& tab) { |
| if (!tab.alert_indicator_button_) { |
| ADD_FAILURE(); |
| return gfx::Rect(); |
| } |
| return tab.alert_indicator_button_->bounds(); |
| } |
| |
| std::string original_locale_; |
| base::SimpleTestTickClock fake_clock_; |
| }; |
| |
| class TabContentsTest : public ChromeViewsTestBase { |
| public: |
| TabContentsTest() = default; |
| TabContentsTest(const TabContentsTest&) = delete; |
| TabContentsTest& operator=(const TabContentsTest&) = delete; |
| ~TabContentsTest() override = default; |
| |
| 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. |
| auto parent = std::make_unique<views::View>(); |
| views::FlexLayout* layout_manager = |
| parent->SetLayoutManager(std::make_unique<views::FlexLayout>()); |
| layout_manager->SetOrientation(views::LayoutOrientation::kHorizontal) |
| .SetDefault( |
| views::kFlexBehaviorKey, |
| views::FlexSpecification(views::MinimumFlexSizeRule::kScaleToZero, |
| views::MaximumFlexSizeRule::kUnbounded)); |
| parent->AddChildViewRaw(tab_strip_.get()); |
| |
| widget_ = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| widget_->SetContentsView(std::move(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_; |
| } |
| |
| base::Time get_camera_mic_indicator_start_time(Tab* tab) { |
| return tab->alert_indicator_button_->camera_mic_indicator_start_time_; |
| } |
| |
| base::TimeDelta get_fadeout_animation_duration_for_testing_(Tab* tab) { |
| return tab->alert_indicator_button_ |
| ->fadeout_animation_duration_for_testing_; |
| } |
| |
| void StopAnimation(Tab* tab) { |
| ASSERT_TRUE(tab->alert_indicator_button_->fade_animation_); |
| tab->alert_indicator_button_->fade_animation_->Stop(); |
| } |
| |
| std::unique_ptr<tab_groups::CollaborationMessagingTabData> |
| GetCollaborationData(ui::UnownedUserDataHost& unowned_user_data_host) { |
| tabs::MockTabInterface mock_tab_interface; |
| MockBrowserWindowInterface mock_browser_window_interface; |
| |
| EXPECT_CALL(mock_tab_interface, GetUnownedUserDataHost()) |
| .Times(1) |
| .WillRepeatedly(testing::ReturnRef(unowned_user_data_host)); |
| |
| EXPECT_CALL(mock_tab_interface, GetBrowserWindowInterface()) |
| .Times(1) |
| .WillRepeatedly(testing::Return(&mock_browser_window_interface)); |
| |
| EXPECT_CALL(mock_browser_window_interface, GetProfile()) |
| .Times(1) |
| .WillRepeatedly(testing::Return(profile())); |
| |
| return std::make_unique<tab_groups::CollaborationMessagingTabData>( |
| &mock_tab_interface); |
| } |
| |
| TestingProfile* profile() { return &profile_; } |
| |
| // Owned by TabStrip. |
| raw_ptr<FakeBaseTabStripController, DanglingUntriaged> controller_ = nullptr; |
| raw_ptr<TabStrip, DanglingUntriaged> tab_strip_ = nullptr; |
| std::unique_ptr<views::Widget> widget_; |
| |
| TestingProfile profile_; |
| }; |
| |
| TEST_F(TabTest, HitTest) { |
| auto tab_slot_controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = |
| widget->SetContentsView(std::make_unique<Tab>(tab_slot_controller.get())); |
| tab->SizeToPreferredSize(); |
| |
| // Attempt to click on the left curved extender. this is not a part of the |
| // hit target. |
| // x ╭─────────╮ |
| // │ Content │ |
| // ┏─╯ ╰─┐ |
| int middle_y = tab->height() / 2; |
| EXPECT_FALSE(tab->HitTestPoint(gfx::Point(0, middle_y))); |
| |
| // Attempt to click above the tab. this is not a part of the hit target. |
| // x |
| // ╭─────────╮ |
| // │ Content │ |
| // ┏─╯ ╰─┐ |
| int middle_x = tab->width() / 2; |
| EXPECT_FALSE(tab->HitTestPoint(gfx::Point(middle_x, -1))); |
| |
| int tab_starting_y = |
| GetLayoutConstant(TAB_STRIP_HEIGHT) - GetLayoutConstant(TAB_HEIGHT); |
| |
| // Attempt to click on the top pixel of the tab. This should be part of the |
| // hit target. |
| // ╭────x────╮ |
| // │ Content │ |
| // ┏─╯ ╰─┐ |
| EXPECT_TRUE(tab->HitTestPoint(gfx::Point(middle_x, tab_starting_y))); |
| |
| // In maximized mode, attempt to click on the top pixel of the tab. This |
| // should be part of the hit target. |
| // ╭────x────╮ |
| // │ Content │ |
| // ┏─╯ ╰─┐ |
| widget->Maximize(); |
| EXPECT_TRUE(tab->HitTestPoint(gfx::Point(middle_x, tab_starting_y))); |
| |
| // Attempt to click on the left curved extender. this is not a part of the |
| // hit target. |
| // x ╭─────────╮ |
| // │ Content │ |
| // ┏─╯ ╰─┐ |
| EXPECT_FALSE(tab->HitTestPoint(gfx::Point(0, tab_starting_y))); |
| |
| // Attempt to click on the right curved extender. this is not a part of the |
| // hit target. |
| // ╭─────────╮ x |
| // │ Content │ |
| // ┏─╯ ╰─┐ |
| EXPECT_FALSE(tab->HitTestPoint(gfx::Point(tab->width() - 1, tab_starting_y))); |
| } |
| |
| TEST_F(TabTest, LayoutAndVisibilityOfElements) { |
| static const std::optional<tabs::TabAlert> kAlertStatesToTest[] = { |
| std::nullopt, |
| tabs::TabAlert::kTabCapturing, |
| tabs::TabAlert::kAudioPlaying, |
| tabs::TabAlert::kAudioMuting, |
| tabs::TabAlert::kPipPlaying, |
| }; |
| |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(16, 16); |
| TabRendererData data; |
| data.favicon = |
| ui::ImageModel::FromImageSkia(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 (std::optional<tabs::TabAlert> alert_state : kAlertStatesToTest) { |
| SCOPED_TRACE( |
| ::testing::Message() |
| << (is_active_tab ? "Active " : "Inactive ") |
| << (is_pinned_tab ? "pinned " : "") |
| << "tab with alert indicator state " |
| << (alert_state ? static_cast<int>(alert_state.value()) : -1)); |
| |
| data.pinned = is_pinned_tab; |
| controller->set_active_tab(is_active_tab ? tab : nullptr); |
| if (alert_state) { |
| data.alert_state = {alert_state.value()}; |
| } else { |
| data.alert_state.clear(); |
| } |
| 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 = |
| tab->tab_style()->GetPinnedWidth(/*is_split=*/false); |
| } else { |
| width = tab->tab_style()->GetStandardWidth(/*is_split=*/false); |
| min_width = |
| is_active_tab |
| ? TabStyle::Get()->GetMinimumActiveWidth(/*is_split=*/false) |
| : TabStyle::Get()->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 layout. |
| CheckForExpectedLayoutAndVisibilityOfElements(*tab); |
| } |
| } |
| } |
| } |
| } |
| |
| // Regression test for http://crbug.com/226253. Performing layout more than once |
| // shouldn't change the insets of the close button. |
| TEST_F(TabTest, CloseButtonLayout) { |
| FakeTabSlotController tab_slot_controller; |
| Tab tab(&tab_slot_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()); |
| } |
| |
| // Regression test for http://crbug.com/609701. Ensure TabCloseButton does not |
| // get focus on right click. |
| TEST_F(TabTest, CloseButtonFocus) { |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| |
| TabCloseButton* tab_close_button = GetCloseButton(tab); |
| |
| // Verify tab_close_button does not get focus on right click. |
| ui::MouseEvent right_click_event(ui::EventType::kKeyPressed, 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()); |
| } |
| |
| #if BUILDFLAG(IS_CHROMEOS) |
| TEST_F(TabTest, CloseButtonHiddenWhenLockedForOnTask) { |
| const auto tab_slot_controller = std::make_unique<FakeTabSlotController>(); |
| tab_slot_controller->SetLockedForOnTask(true); |
| const std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET); |
| Tab* const tab = |
| widget->SetContentsView(std::make_unique<Tab>(tab_slot_controller.get())); |
| TabCloseButton* const tab_close_button = GetCloseButton(tab); |
| EXPECT_FALSE(tab_close_button->GetVisible()); |
| } |
| |
| TEST_F(TabTest, CloseButtonShownWhenNotLockedForOnTask) { |
| const auto tab_slot_controller = std::make_unique<FakeTabSlotController>(); |
| tab_slot_controller->SetLockedForOnTask(false); |
| const std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::CLIENT_OWNS_WIDGET); |
| Tab* const tab = |
| widget->SetContentsView(std::make_unique<Tab>(tab_slot_controller.get())); |
| TabCloseButton* const tab_close_button = GetCloseButton(tab); |
| EXPECT_TRUE(tab_close_button->GetVisible()); |
| } |
| #endif |
| |
| // 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) { |
| auto tab_slot_controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = |
| widget->SetContentsView(std::make_unique<Tab>(tab_slot_controller.get())); |
| tab->SizeToPreferredSize(); |
| |
| TabIcon* icon = GetTabIcon(tab); |
| SetupFakeClock(icon); |
| TabRendererData data; |
| data.visible_url = GURL("http://example.com"); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| 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_slot_controller->CanPaintThrobberToLayer()); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kLoading; |
| tab->SetData(data); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| |
| // Simulate a tab that should hide throbber. |
| data.should_hide_throbber = true; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| data.network_state = TabNetworkState::kWaiting; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| data.network_state = TabNetworkState::kLoading; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| data.network_state = TabNetworkState::kNone; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| |
| // Simulate a tab that should not hide throbber. |
| data.should_hide_throbber = false; |
| data.network_state = TabNetworkState::kWaiting; |
| tab->SetData(data); |
| EXPECT_TRUE(tab_slot_controller->CanPaintThrobberToLayer()); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kLoading; |
| tab->SetData(data); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| |
| // After loading is done, simulate another resource starting to load. |
| data.network_state = TabNetworkState::kWaiting; |
| tab->SetData(data); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| |
| // Reset. |
| data.network_state = TabNetworkState::kNone; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| |
| // Simulate a drag started and stopped during a load: layer painting stops |
| // temporarily. |
| data.network_state = TabNetworkState::kWaiting; |
| tab->SetData(data); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| tab_slot_controller->set_paint_throbber_to_layer(false); |
| tab->StepLoadingAnimation(base::Milliseconds(100)); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_FALSE(icon->layer()); |
| tab_slot_controller->set_paint_throbber_to_layer(true); |
| tab->StepLoadingAnimation(base::Milliseconds(100)); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_TRUE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| |
| // Simulate a tab load starting and stopping during tab dragging: |
| // no layer painting. |
| tab_slot_controller->set_paint_throbber_to_layer(false); |
| data.network_state = TabNetworkState::kWaiting; |
| tab->SetData(data); |
| EXPECT_TRUE(icon->GetShowingLoadingAnimation()); |
| EXPECT_FALSE(icon->layer()); |
| data.network_state = TabNetworkState::kNone; |
| tab->SetData(data); |
| EXPECT_FALSE(icon->GetShowingLoadingAnimation()); |
| } |
| |
| TEST_F(TabTest, TitleHiddenWhenSmall) { |
| FakeTabSlotController tab_slot_controller; |
| Tab tab(&tab_slot_controller); |
| 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) { |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| |
| for (bool is_active_tab : {false, true}) { |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| controller->set_active_tab(is_active_tab ? tab : nullptr); |
| tab->SizeToPreferredSize(); |
| |
| views::View* icon = GetTabIcon(tab); |
| int icon_x = icon->x(); |
| TabRendererData data; |
| data.alert_state = {tabs::TabAlert::kAudioPlaying}; |
| tab->SetData(data); |
| EXPECT_EQ(icon_x, icon->x()); |
| } |
| } |
| |
| TEST_F(TabTest, SmallTabsHideCloseButton) { |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| const int width = tab->tab_style_views()->GetContentsInsets().width() + |
| Tab::kMinimumContentsWidthForCloseButtons; |
| tab->SetBounds(0, 0, width, 50); |
| const views::View* close = GetCloseButton(tab); |
| EXPECT_TRUE(close->GetVisible()); |
| |
| // Shrink the tab. The close button should disappear. |
| tab->SetBounds(0, 0, width - 1, 50); |
| EXPECT_FALSE(close->GetVisible()); |
| } |
| |
| TEST_F(TabTest, ExtraLeftPaddingShownOnSiteWithoutFavicon) { |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| |
| tab->SizeToPreferredSize(); |
| const views::View* icon = GetTabIcon(tab); |
| const int icon_x = icon->x() + icon->GetInsets().left(); |
| |
| // Remove the favicon. |
| TabRendererData data; |
| data.show_icon = false; |
| tab->SetData(data); |
| EndTitleAnimation(tab); |
| EXPECT_FALSE(icon->GetVisible()); |
| // Title should be placed where the favicon was. |
| EXPECT_EQ(icon_x, GetTabTitle(tab)->x()); |
| } |
| |
| TEST_F(TabTest, ExtraAlertPaddingNotShownOnSmallActiveTab) { |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| controller->set_active_tab(tab); |
| TabRendererData data; |
| data.alert_state = {tabs::TabAlert::kAudioPlaying}; |
| tab->SetData(data); |
| |
| tab->SetBounds(0, 0, 200, 50); |
| EXPECT_TRUE(GetTabIcon(tab)->GetVisible()); |
| 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, 90, 50); |
| EXPECT_FALSE(GetTabIcon(tab)->GetVisible()); |
| |
| tab->SetBounds(0, 0, 76, 50); |
| EXPECT_TRUE(close->GetVisible()); |
| EXPECT_TRUE(alert->GetVisible()); |
| |
| // The alert indicator moves closer because the extra padding is gone. |
| EXPECT_LT(close->x() - alert->bounds().right(), original_spacing); |
| |
| tab->SetBounds(0, 0, 75, 50); |
| EXPECT_TRUE(close->GetVisible()); |
| EXPECT_FALSE(alert->GetVisible()); |
| } |
| |
| 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, |
| }, |
| }; |
| |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| // Create a tab inside a Widget, so it has a theme provider, so the call to |
| // UpdateForegroundColors() below doesn't no-op. |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| |
| for (const auto& colors : color_schemes) { |
| tab->GetColorProvider()->SetColorForTesting( |
| kColorTabBackgroundActiveFrameActive, colors.bg_active); |
| tab->GetColorProvider()->SetColorForTesting( |
| kColorTabBackgroundActiveFrameInactive, colors.bg_active); |
| tab->GetColorProvider()->SetColorForTesting( |
| kColorTabBackgroundInactiveFrameActive, colors.bg_inactive); |
| tab->GetColorProvider()->SetColorForTesting( |
| kColorTabBackgroundInactiveFrameInactive, colors.bg_inactive); |
| controller->SetTabColors(colors.fg_active, colors.fg_inactive); |
| for (TabActive active : {TabActive::kInactive, TabActive::kActive}) { |
| controller->set_active_tab(active == TabActive::kActive ? tab : nullptr); |
| tab->UpdateForegroundColors(); |
| const SkColor fg_color = tab->title_->GetEnabledColor(); |
| const SkColor bg_color = TabStyle::Get()->GetTabBackgroundColor( |
| active == TabActive::kActive ? TabStyle::TabSelectionState::kActive |
| : TabStyle::TabSelectionState::kInactive, |
| /*hovered=*/false, tab->GetWidget()->ShouldPaintAsActive(), |
| *tab->GetColorProvider()); |
| 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(TabContentsTest, ShowsAndHidesAlertIndicator) { |
| controller_->AddTab(0, TabActive::kInactive, TabPinned::kPinned); |
| controller_->AddTab(1, TabActive::kActive); |
| 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 = {tabs::TabAlert::kAudioPlaying}; |
| 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.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)); |
| } |
| |
| // This test verifies that the alert indicator for a camera and/or mic is |
| // visible at least for 5 seconds even if a camera/mic stopped being used. |
| TEST_F(TabContentsTest, MinHoldDurationTest) { |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| controller_->AddTab(0, TabActive::kActive); |
| Tab* media_tab = tab_strip_->tab_at(0); |
| |
| EXPECT_FALSE(showing_alert_indicator(media_tab)); |
| |
| EXPECT_EQ(base::Time(), get_camera_mic_indicator_start_time(media_tab)); |
| |
| TabRendererData start_media; |
| start_media.alert_state = {tabs::TabAlert::kMediaRecording}; |
| start_media.pinned = media_tab->data().pinned; |
| media_tab->SetData(std::move(start_media)); |
| |
| // When audio starts, pinned inactive tab shows indicator. |
| EXPECT_TRUE(showing_alert_indicator(media_tab)); |
| EXPECT_NE(base::Time(), get_camera_mic_indicator_start_time(media_tab)); |
| |
| TabRendererData stop_media; |
| stop_media.pinned = media_tab->data().pinned; |
| media_tab->SetData(std::move(stop_media)); |
| |
| // The indicator's start time should be reset. |
| EXPECT_EQ(base::Time(), get_camera_mic_indicator_start_time(media_tab)); |
| EXPECT_EQ(base::Seconds(5), |
| get_fadeout_animation_duration_for_testing_(media_tab)); |
| } |
| |
| // This test verifies that the alert indicator for a camera and/or mic has |
| // 1-second fadeout animation after it was visible for longer than 5 seconds. |
| TEST_F(TabContentsTest, 1SecondFadeoutAnimationTest) { |
| base::test::ScopedFeatureList scoped_feature_list_; |
| |
| controller_->AddTab(0, TabActive::kActive); |
| Tab* media_tab = tab_strip_->tab_at(0); |
| |
| EXPECT_FALSE(showing_alert_indicator(media_tab)); |
| |
| EXPECT_EQ(base::Time(), get_camera_mic_indicator_start_time(media_tab)); |
| |
| TabRendererData start_media; |
| start_media.alert_state = {tabs::TabAlert::kMediaRecording}; |
| start_media.pinned = media_tab->data().pinned; |
| media_tab->SetData(std::move(start_media)); |
| |
| // When audio starts, pinned inactive tab shows indicator. |
| EXPECT_TRUE(showing_alert_indicator(media_tab)); |
| EXPECT_NE(base::Time(), get_camera_mic_indicator_start_time(media_tab)); |
| |
| // After the indicator was displayed for 6 seconds, it should have 1-second |
| // fadeout animation. |
| task_environment()->AdvanceClock(base::Seconds(6)); |
| base::RunLoop().RunUntilIdle(); |
| |
| TabRendererData stop_media; |
| stop_media.pinned = media_tab->data().pinned; |
| media_tab->SetData(std::move(stop_media)); |
| |
| // The indicator's start time should be reset. |
| EXPECT_EQ(base::Time(), get_camera_mic_indicator_start_time(media_tab)); |
| EXPECT_EQ(base::Seconds(1), |
| get_fadeout_animation_duration_for_testing_(media_tab)); |
| } |
| |
| TEST_F(TabTest, DiscardIndicatorResponsiveness) { |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| const TabIcon* tab_icon = GetTabIcon(tab); |
| |
| struct TestCase { |
| int tab_width; |
| int expected_increased_radius; |
| }; |
| std::list<TestCase> test_cases{ |
| {256, 2}, {45, 2}, {44, 2}, {43, 0}, {32, 0}, |
| }; |
| |
| for (auto const& test_case : test_cases) { |
| tab->SetBounds(0, 0, test_case.tab_width, 50); |
| EXPECT_EQ(test_case.expected_increased_radius, |
| tab_icon->increased_discard_indicator_radius_); |
| } |
| } |
| |
| TEST_F(TabTest, AccessibleProperties) { |
| auto controller = std::make_unique<FakeTabSlotController>(); |
| std::unique_ptr<views::Widget> widget = |
| CreateTestWidget(views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET); |
| Tab* tab = widget->SetContentsView(std::make_unique<Tab>(controller.get())); |
| ui::AXNodeData data; |
| |
| tab->GetViewAccessibility().GetAccessibleNodeData(&data); |
| EXPECT_EQ(ax::mojom::Role::kTab, data.role); |
| } |
| |
| TEST_F(TabContentsTest, AccessibleNameChanged) { |
| controller_->AddTab(0, TabActive::kInactive, TabPinned::kPinned); |
| |
| TabRendererData old_data = tab_strip_->tab_at(0)->data(); |
| TabRendererData new_data = tab_strip_->tab_at(0)->data(); |
| EXPECT_FALSE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| |
| EXPECT_FALSE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| |
| new_data.title = u"new_title"; |
| EXPECT_TRUE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| } |
| |
| TEST_F(TabContentsTest, AccessibleNameChangesWithCollaborationMessages) { |
| controller_->AddTab(0, TabActive::kInactive, TabPinned::kPinned); |
| |
| TabRendererData old_data = tab_strip_->tab_at(0)->data(); |
| TabRendererData new_data = tab_strip_->tab_at(0)->data(); |
| EXPECT_FALSE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| |
| // Create message for new_data. |
| ui::UnownedUserDataHost unowned_user_data_1; |
| std::unique_ptr<tab_groups::CollaborationMessagingTabData> |
| collaboration_messaging1 = GetCollaborationData(unowned_user_data_1); |
| collaboration_messaging1->set_mocked_avatar_for_testing(gfx::Image()); |
| collaboration_messaging1->SetMessage( |
| CreateMessage("Name1", CollaborationEvent::TAB_ADDED)); |
| new_data.collaboration_messaging = collaboration_messaging1->GetWeakPtr(); |
| |
| EXPECT_TRUE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| |
| // Create message with a different name for old_data. |
| ui::UnownedUserDataHost unowned_user_data_2; |
| std::unique_ptr<tab_groups::CollaborationMessagingTabData> |
| collaboration_messaging2 = GetCollaborationData(unowned_user_data_2); |
| collaboration_messaging2->set_mocked_avatar_for_testing(gfx::Image()); |
| collaboration_messaging2->SetMessage( |
| CreateMessage("Name2", CollaborationEvent::TAB_ADDED)); |
| old_data.collaboration_messaging = collaboration_messaging2->GetWeakPtr(); |
| |
| EXPECT_TRUE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| |
| // Create message with a different event for old_data. |
| ui::UnownedUserDataHost unowned_user_data_3; |
| std::unique_ptr<tab_groups::CollaborationMessagingTabData> |
| collaboration_messaging3 = GetCollaborationData(unowned_user_data_3); |
| collaboration_messaging3->set_mocked_avatar_for_testing(gfx::Image()); |
| collaboration_messaging3->SetMessage( |
| CreateMessage("Name1", CollaborationEvent::TAB_UPDATED)); |
| old_data.collaboration_messaging = collaboration_messaging3->GetWeakPtr(); |
| |
| EXPECT_TRUE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| |
| // Create a duplicate message for old_data. |
| ui::UnownedUserDataHost unowned_user_data_4; |
| std::unique_ptr<tab_groups::CollaborationMessagingTabData> |
| collaboration_messaging4 = GetCollaborationData(unowned_user_data_4); |
| collaboration_messaging4->set_mocked_avatar_for_testing(gfx::Image()); |
| collaboration_messaging4->SetMessage( |
| CreateMessage("Name1", CollaborationEvent::TAB_ADDED)); |
| old_data.collaboration_messaging = collaboration_messaging4->GetWeakPtr(); |
| |
| EXPECT_FALSE( |
| tab_strip_->tab_at(0)->ShouldUpdateAccessibleName(old_data, new_data)); |
| } |