1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "chrome/browser/ui/views/tabs/tab.h"
7 #include "base/i18n/rtl.h"
8 #include "base/strings/utf_string_conversions.h"
9 #include "chrome/browser/ui/tabs/tab_utils.h"
10 #include "chrome/browser/ui/views/tabs/media_indicator_button.h"
11 #include "chrome/browser/ui/views/tabs/tab_controller.h"
12 #include "testing/gtest/include/gtest/gtest.h"
13 #include "ui/base/models/list_selection_model.h"
14 #include "ui/views/controls/button/image_button.h"
15 #include "ui/views/controls/label.h"
16 #include "ui/views/test/views_test_base.h"
17 #include "ui/views/widget/widget.h"
21 class FakeTabController
: public TabController
{
23 FakeTabController() : immersive_style_(false), active_tab_(false) {
25 ~FakeTabController() override
{}
27 void set_immersive_style(bool value
) { immersive_style_
= value
; }
28 void set_active_tab(bool value
) { active_tab_
= value
; }
30 const ui::ListSelectionModel
& GetSelectionModel() override
{
31 return selection_model_
;
33 bool SupportsMultipleSelection() override
{ return false; }
34 bool ShouldHideCloseButtonForInactiveTabs() override
{
37 void SelectTab(Tab
* tab
) override
{}
38 void ExtendSelectionTo(Tab
* tab
) override
{}
39 void ToggleSelected(Tab
* tab
) override
{}
40 void AddSelectionFromAnchorTo(Tab
* tab
) override
{}
41 void CloseTab(Tab
* tab
, CloseTabSource source
) override
{}
42 void ToggleTabAudioMute(Tab
* tab
) override
{}
43 void ShowContextMenuForTab(Tab
* tab
,
45 ui::MenuSourceType source_type
) override
{}
46 bool IsActiveTab(const Tab
* tab
) const override
{ return active_tab_
; }
47 bool IsTabSelected(const Tab
* tab
) const override
{ return false; }
48 bool IsTabPinned(const Tab
* tab
) const override
{ return false; }
51 const ui::LocatedEvent
& event
,
52 const ui::ListSelectionModel
& original_selection
) override
{}
53 void ContinueDrag(views::View
* view
, const ui::LocatedEvent
& event
) override
{
55 bool EndDrag(EndDragReason reason
) override
{ return false; }
56 Tab
* GetTabAt(Tab
* tab
, const gfx::Point
& tab_in_tab_coordinates
) override
{
59 void OnMouseEventInTab(views::View
* source
,
60 const ui::MouseEvent
& event
) override
{}
61 bool ShouldPaintTab(const Tab
* tab
, gfx::Rect
* clip
) override
{ return true; }
62 bool IsImmersiveStyle() const override
{ return immersive_style_
; }
63 void UpdateTabAccessibilityState(const Tab
* tab
,
64 ui::AXViewState
* state
) override
{};
67 ui::ListSelectionModel selection_model_
;
68 bool immersive_style_
;
71 DISALLOW_COPY_AND_ASSIGN(FakeTabController
);
74 class TabTest
: public views::ViewsTestBase
,
75 public ::testing::WithParamInterface
<bool> {
80 bool testing_for_rtl_locale() const { return GetParam(); }
82 void SetUp() override
{
83 if (testing_for_rtl_locale()) {
84 original_locale_
= base::i18n::GetConfiguredLocale();
85 base::i18n::SetICUDefaultLocale("he");
87 views::ViewsTestBase::SetUp();
90 void TearDown() override
{
91 views::ViewsTestBase::TearDown();
92 if (testing_for_rtl_locale())
93 base::i18n::SetICUDefaultLocale(original_locale_
);
96 static void CheckForExpectedLayoutAndVisibilityOfElements(const Tab
& tab
) {
97 // Check whether elements are visible when they are supposed to be, given
98 // Tab size and TabRendererData state.
99 if (tab
.data_
.pinned
) {
100 EXPECT_EQ(1, tab
.IconCapacity());
101 if (tab
.data_
.media_state
!= TAB_MEDIA_STATE_NONE
) {
102 EXPECT_FALSE(tab
.ShouldShowIcon());
103 EXPECT_TRUE(tab
.ShouldShowMediaIndicator());
105 EXPECT_TRUE(tab
.ShouldShowIcon());
106 EXPECT_FALSE(tab
.ShouldShowMediaIndicator());
108 EXPECT_FALSE(tab
.ShouldShowCloseBox());
109 } else if (tab
.IsActive()) {
110 EXPECT_TRUE(tab
.ShouldShowCloseBox());
111 switch (tab
.IconCapacity()) {
114 EXPECT_FALSE(tab
.ShouldShowIcon());
115 EXPECT_FALSE(tab
.ShouldShowMediaIndicator());
118 if (tab
.data_
.media_state
!= TAB_MEDIA_STATE_NONE
) {
119 EXPECT_FALSE(tab
.ShouldShowIcon());
120 EXPECT_TRUE(tab
.ShouldShowMediaIndicator());
122 EXPECT_TRUE(tab
.ShouldShowIcon());
123 EXPECT_FALSE(tab
.ShouldShowMediaIndicator());
127 EXPECT_LE(3, tab
.IconCapacity());
128 EXPECT_TRUE(tab
.ShouldShowIcon());
129 if (tab
.data_
.media_state
!= TAB_MEDIA_STATE_NONE
)
130 EXPECT_TRUE(tab
.ShouldShowMediaIndicator());
132 EXPECT_FALSE(tab
.ShouldShowMediaIndicator());
135 } else { // Tab not active and not pinned tab.
136 switch (tab
.IconCapacity()) {
138 EXPECT_FALSE(tab
.ShouldShowCloseBox());
139 EXPECT_FALSE(tab
.ShouldShowIcon());
140 EXPECT_FALSE(tab
.ShouldShowMediaIndicator());
143 EXPECT_FALSE(tab
.ShouldShowCloseBox());
144 if (tab
.data_
.media_state
!= TAB_MEDIA_STATE_NONE
) {
145 EXPECT_FALSE(tab
.ShouldShowIcon());
146 EXPECT_TRUE(tab
.ShouldShowMediaIndicator());
148 EXPECT_TRUE(tab
.ShouldShowIcon());
149 EXPECT_FALSE(tab
.ShouldShowMediaIndicator());
153 EXPECT_LE(2, tab
.IconCapacity());
154 EXPECT_TRUE(tab
.ShouldShowIcon());
155 if (tab
.data_
.media_state
!= TAB_MEDIA_STATE_NONE
)
156 EXPECT_TRUE(tab
.ShouldShowMediaIndicator());
158 EXPECT_FALSE(tab
.ShouldShowMediaIndicator());
163 // Check positioning of elements with respect to each other, and that they
164 // are fully within the contents bounds.
165 const gfx::Rect contents_bounds
= tab
.GetContentsBounds();
166 if (tab
.ShouldShowIcon()) {
167 EXPECT_LE(contents_bounds
.x(), tab
.favicon_bounds_
.x());
168 if (tab
.title_
->width() > 0)
169 EXPECT_LE(tab
.favicon_bounds_
.right(), tab
.title_
->x());
170 EXPECT_LE(contents_bounds
.y(), tab
.favicon_bounds_
.y());
171 EXPECT_LE(tab
.favicon_bounds_
.bottom(), contents_bounds
.bottom());
173 if (tab
.ShouldShowIcon() && tab
.ShouldShowMediaIndicator())
174 EXPECT_LE(tab
.favicon_bounds_
.right(), GetMediaIndicatorBounds(tab
).x());
175 if (tab
.ShouldShowMediaIndicator()) {
176 if (tab
.title_
->width() > 0) {
177 EXPECT_LE(tab
.title_
->bounds().right(),
178 GetMediaIndicatorBounds(tab
).x());
180 EXPECT_LE(GetMediaIndicatorBounds(tab
).right(), contents_bounds
.right());
181 EXPECT_LE(contents_bounds
.y(), GetMediaIndicatorBounds(tab
).y());
182 EXPECT_LE(GetMediaIndicatorBounds(tab
).bottom(),
183 contents_bounds
.bottom());
185 if (tab
.ShouldShowMediaIndicator() && tab
.ShouldShowCloseBox()) {
186 // Note: The media indicator can overlap the left-insets of the close box,
187 // but should otherwise be to the left of the close button.
188 EXPECT_LE(GetMediaIndicatorBounds(tab
).right(),
189 tab
.close_button_
->bounds().x() +
190 tab
.close_button_
->GetInsets().left());
192 if (tab
.ShouldShowCloseBox()) {
193 // Note: The title bounds can overlap the left-insets of the close box,
194 // but should otherwise be to the left of the close button.
195 if (tab
.title_
->width() > 0) {
196 EXPECT_LE(tab
.title_
->bounds().right(),
197 tab
.close_button_
->bounds().x() +
198 tab
.close_button_
->GetInsets().left());
200 EXPECT_LE(tab
.close_button_
->bounds().right(), contents_bounds
.right());
201 EXPECT_LE(contents_bounds
.y(), tab
.close_button_
->bounds().y());
202 EXPECT_LE(tab
.close_button_
->bounds().bottom(), contents_bounds
.bottom());
207 void InitWidget(Widget
* widget
) {
208 Widget::InitParams
params(CreateParams(Widget::InitParams::TYPE_WINDOW
));
209 params
.ownership
= Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET
;
210 params
.bounds
.SetRect(10, 20, 300, 400);
211 widget
->Init(params
);
215 static gfx::Rect
GetMediaIndicatorBounds(const Tab
& tab
) {
216 if (!tab
.media_indicator_button_
) {
220 return tab
.media_indicator_button_
->bounds();
223 std::string original_locale_
;
226 TEST_P(TabTest
, HitTestTopPixel
) {
227 if (testing_for_rtl_locale() && !base::i18n::IsRTL()) {
228 LOG(WARNING
) << "Testing of RTL locale not supported on current platform.";
235 FakeTabController tab_controller
;
236 Tab
tab(&tab_controller
);
237 widget
.GetContentsView()->AddChildView(&tab
);
238 tab
.SetBoundsRect(gfx::Rect(gfx::Point(0, 0), Tab::GetStandardSize()));
240 // Tabs have some shadow in the top, so by default we don't hit the tab there.
241 int middle_x
= tab
.width() / 2;
242 EXPECT_FALSE(tab
.HitTestPoint(gfx::Point(middle_x
, 0)));
244 // Tabs are slanted, so a click halfway down the left edge won't hit it.
245 int middle_y
= tab
.height() / 2;
246 EXPECT_FALSE(tab
.HitTestPoint(gfx::Point(0, middle_y
)));
248 // If the window is maximized, however, we want clicks in the top edge to
251 EXPECT_TRUE(tab
.HitTestPoint(gfx::Point(middle_x
, 0)));
253 // But clicks in the area above the slanted sides should still miss.
254 EXPECT_FALSE(tab
.HitTestPoint(gfx::Point(0, 0)));
255 EXPECT_FALSE(tab
.HitTestPoint(gfx::Point(tab
.width() - 1, 0)));
258 TEST_P(TabTest
, LayoutAndVisibilityOfElements
) {
259 if (testing_for_rtl_locale() && !base::i18n::IsRTL()) {
260 LOG(WARNING
) << "Testing of RTL locale not supported on current platform.";
264 static const TabMediaState kMediaStatesToTest
[] = {
265 TAB_MEDIA_STATE_NONE
, TAB_MEDIA_STATE_CAPTURING
,
266 TAB_MEDIA_STATE_AUDIO_PLAYING
, TAB_MEDIA_STATE_AUDIO_MUTING
272 FakeTabController controller
;
273 Tab
tab(&controller
);
274 widget
.GetContentsView()->AddChildView(&tab
);
277 bitmap
.allocN32Pixels(16, 16);
278 TabRendererData data
;
279 data
.favicon
= gfx::ImageSkia::CreateFrom1xBitmap(bitmap
);
281 // Perform layout over all possible combinations, checking for correct
283 for (int is_pinned_tab
= 0; is_pinned_tab
< 2; ++is_pinned_tab
) {
284 for (int is_active_tab
= 0; is_active_tab
< 2; ++is_active_tab
) {
285 for (size_t media_state_index
= 0;
286 media_state_index
< arraysize(kMediaStatesToTest
);
287 ++media_state_index
) {
288 const TabMediaState media_state
= kMediaStatesToTest
[media_state_index
];
289 SCOPED_TRACE(::testing::Message()
290 << (is_active_tab
? "Active" : "Inactive") << ' '
291 << (is_pinned_tab
? "Pinned " : "")
292 << "Tab with media indicator state " << media_state
);
294 data
.pinned
= !!is_pinned_tab
;
295 controller
.set_active_tab(!!is_active_tab
);
296 data
.media_state
= media_state
;
299 // Test layout for every width from standard to minimum.
300 gfx::Rect
bounds(gfx::Point(0, 0), Tab::GetStandardSize());
303 bounds
.set_width(Tab::GetPinnedWidth());
304 min_width
= Tab::GetPinnedWidth();
306 min_width
= is_active_tab
? Tab::GetMinimumSelectedSize().width() :
307 Tab::GetMinimumUnselectedSize().width();
309 while (bounds
.width() >= min_width
) {
310 SCOPED_TRACE(::testing::Message() << "bounds=" << bounds
.ToString());
311 tab
.SetBoundsRect(bounds
); // Invokes Tab::Layout().
312 CheckForExpectedLayoutAndVisibilityOfElements(tab
);
313 bounds
.set_width(bounds
.width() - 1);
320 // Regression test for http://crbug.com/420313: Confirms that any child Views of
321 // Tab do not attempt to provide their own tooltip behavior/text. It also tests
322 // that Tab provides the expected tooltip text (according to tab_utils).
323 TEST_P(TabTest
, TooltipProvidedByTab
) {
324 if (testing_for_rtl_locale() && !base::i18n::IsRTL()) {
325 LOG(WARNING
) << "Testing of RTL locale not supported on current platform.";
332 FakeTabController controller
;
333 Tab
tab(&controller
);
334 widget
.GetContentsView()->AddChildView(&tab
);
335 tab
.SetBoundsRect(gfx::Rect(Tab::GetStandardSize()));
338 bitmap
.allocN32Pixels(16, 16);
339 TabRendererData data
;
340 data
.favicon
= gfx::ImageSkia::CreateFrom1xBitmap(bitmap
);
342 data
.title
= base::UTF8ToUTF16(
343 "This is a really long tab title that would case views::Label to provide "
344 "its own tooltip; but Tab should disable that feature so it can provide "
345 "the tooltip instead.");
347 // Test both with and without an indicator showing since the tab tooltip text
348 // should include a description of the media state when the indicator is
350 for (int i
= 0; i
< 2; ++i
) {
352 (i
== 0 ? TAB_MEDIA_STATE_NONE
: TAB_MEDIA_STATE_AUDIO_PLAYING
);
353 SCOPED_TRACE(::testing::Message()
354 << "Tab with media indicator state " << data
.media_state
);
357 for (int j
= 0; j
< tab
.child_count(); ++j
) {
358 views::View
& child
= *tab
.child_at(j
);
359 if (!strcmp(child
.GetClassName(), "TabCloseButton"))
360 continue; // Close button is excepted.
361 if (!child
.visible())
363 SCOPED_TRACE(::testing::Message() << "child_at(" << j
<< "): "
364 << child
.GetClassName());
366 const gfx::Point
midpoint(child
.width() / 2, child
.height() / 2);
367 EXPECT_FALSE(child
.GetTooltipHandlerForPoint(midpoint
));
368 const gfx::Point mouse_hover_point
=
369 midpoint
+ child
.GetMirroredPosition().OffsetFromOrigin();
370 base::string16 tooltip
;
371 EXPECT_TRUE(static_cast<views::View
&>(tab
).GetTooltipText(
372 mouse_hover_point
, &tooltip
));
373 EXPECT_EQ(chrome::AssembleTabTooltipText(data
.title
, data
.media_state
),
379 // Regression test for http://crbug.com/226253. Calling Layout() more than once
380 // shouldn't change the insets of the close button.
381 TEST_P(TabTest
, CloseButtonLayout
) {
382 if (testing_for_rtl_locale() && !base::i18n::IsRTL()) {
383 LOG(WARNING
) << "Testing of RTL locale not supported on current platform.";
387 FakeTabController tab_controller
;
388 Tab
tab(&tab_controller
);
389 tab
.SetBounds(0, 0, 100, 50);
391 gfx::Insets close_button_insets
= tab
.close_button_
->GetInsets();
393 gfx::Insets close_button_insets_2
= tab
.close_button_
->GetInsets();
394 EXPECT_EQ(close_button_insets
.top(), close_button_insets_2
.top());
395 EXPECT_EQ(close_button_insets
.left(), close_button_insets_2
.left());
396 EXPECT_EQ(close_button_insets
.bottom(), close_button_insets_2
.bottom());
397 EXPECT_EQ(close_button_insets
.right(), close_button_insets_2
.right());
399 // Also make sure the close button is sized as large as the tab.
400 EXPECT_EQ(50, tab
.close_button_
->bounds().height());
403 // Test in both a LTR and a RTL locale. Note: The fact that the UI code is
404 // configured for an RTL locale does *not* change how the coordinates are
405 // examined in the tests above because views::View and friends are supposed to
406 // auto-mirror the widgets when painting. Thus, what we're testing here is that
407 // there's no code in Tab that will erroneously subvert this automatic
408 // coordinate translation. http://crbug.com/384179
409 INSTANTIATE_TEST_CASE_P(, TabTest
, ::testing::Values(false, true));