blob: fc90aef3bdaf5aa6a521ed852382e7a03573f78f [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ui/views/tabs/tab.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));
}