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. 4 5 #include "chrome/browser/ui/views/tabs/tab.h" 6 7 #include "base/i18n/rtl.h" 8 #include "base/strings/utf_string_conversions.h" 9 #include "chrome/browser/ui/views/tabs/media_indicator_button.h" 10 #include "chrome/browser/ui/views/tabs/tab_controller.h" 11 #include "testing/gtest/include/gtest/gtest.h" 12 #include "ui/base/models/list_selection_model.h" 13 #include "ui/views/controls/button/image_button.h" 14 #include "ui/views/controls/label.h" 15 #include "ui/views/test/views_test_base.h" 16 #include "ui/views/widget/widget.h" 17 18 using views::Widget; 19 20 class FakeTabController : public TabController { 21 public: 22 FakeTabController() : immersive_style_(false), active_tab_(false) { 23 } 24 virtual ~FakeTabController() {} 25 26 void set_immersive_style(bool value) { immersive_style_ = value; } 27 void set_active_tab(bool value) { active_tab_ = value; } 28 29 virtual const ui::ListSelectionModel& GetSelectionModel() OVERRIDE { 30 return selection_model_; 31 } 32 virtual bool SupportsMultipleSelection() OVERRIDE { return false; } 33 virtual void SelectTab(Tab* tab) OVERRIDE {} 34 virtual void ExtendSelectionTo(Tab* tab) OVERRIDE {} 35 virtual void ToggleSelected(Tab* tab) OVERRIDE {} 36 virtual void AddSelectionFromAnchorTo(Tab* tab) OVERRIDE {} 37 virtual void CloseTab(Tab* tab, CloseTabSource source) OVERRIDE {} 38 virtual void ToggleTabAudioMute(Tab* tab) OVERRIDE {} 39 virtual void ShowContextMenuForTab(Tab* tab, 40 const gfx::Point& p, 41 ui::MenuSourceType source_type) OVERRIDE {} 42 virtual bool IsActiveTab(const Tab* tab) const OVERRIDE { 43 return active_tab_; 44 } 45 virtual bool IsTabSelected(const Tab* tab) const OVERRIDE { 46 return false; 47 } 48 virtual bool IsTabPinned(const Tab* tab) const OVERRIDE { return false; } 49 virtual void MaybeStartDrag( 50 Tab* tab, 51 const ui::LocatedEvent& event, 52 const ui::ListSelectionModel& original_selection) OVERRIDE {} 53 virtual void ContinueDrag(views::View* view, 54 const ui::LocatedEvent& event) OVERRIDE {} 55 virtual bool EndDrag(EndDragReason reason) OVERRIDE { return false; } 56 virtual Tab* GetTabAt(Tab* tab, 57 const gfx::Point& tab_in_tab_coordinates) OVERRIDE { 58 return NULL; 59 } 60 virtual void OnMouseEventInTab(views::View* source, 61 const ui::MouseEvent& event) OVERRIDE {} 62 virtual bool ShouldPaintTab(const Tab* tab, gfx::Rect* clip) OVERRIDE { 63 return true; 64 } 65 virtual bool IsImmersiveStyle() const OVERRIDE { return immersive_style_; } 66 virtual void UpdateTabAccessibilityState(const Tab* tab, 67 ui::AXViewState* state) OVERRIDE{}; 68 69 private: 70 ui::ListSelectionModel selection_model_; 71 bool immersive_style_; 72 bool active_tab_; 73 74 DISALLOW_COPY_AND_ASSIGN(FakeTabController); 75 }; 76 77 class TabTest : public views::ViewsTestBase, 78 public ::testing::WithParamInterface<bool> { 79 public: 80 TabTest() {} 81 virtual ~TabTest() {} 82 83 bool testing_for_rtl_locale() const { return GetParam(); } 84 85 virtual void SetUp() OVERRIDE { 86 if (testing_for_rtl_locale()) { 87 original_locale_ = base::i18n::GetConfiguredLocale(); 88 base::i18n::SetICUDefaultLocale("he"); 89 } 90 views::ViewsTestBase::SetUp(); 91 } 92 93 virtual void TearDown() OVERRIDE { 94 views::ViewsTestBase::TearDown(); 95 if (testing_for_rtl_locale()) 96 base::i18n::SetICUDefaultLocale(original_locale_); 97 } 98 99 static void CheckForExpectedLayoutAndVisibilityOfElements(const Tab& tab) { 100 // Check whether elements are visible when they are supposed to be, given 101 // Tab size and TabRendererData state. 102 if (tab.data_.mini) { 103 EXPECT_EQ(1, tab.IconCapacity()); 104 if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) { 105 EXPECT_FALSE(tab.ShouldShowIcon()); 106 EXPECT_TRUE(tab.ShouldShowMediaIndicator()); 107 } else { 108 EXPECT_TRUE(tab.ShouldShowIcon()); 109 EXPECT_FALSE(tab.ShouldShowMediaIndicator()); 110 } 111 EXPECT_FALSE(tab.ShouldShowCloseBox()); 112 } else if (tab.IsActive()) { 113 EXPECT_TRUE(tab.ShouldShowCloseBox()); 114 switch (tab.IconCapacity()) { 115 case 0: 116 case 1: 117 EXPECT_FALSE(tab.ShouldShowIcon()); 118 EXPECT_FALSE(tab.ShouldShowMediaIndicator()); 119 break; 120 case 2: 121 if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) { 122 EXPECT_FALSE(tab.ShouldShowIcon()); 123 EXPECT_TRUE(tab.ShouldShowMediaIndicator()); 124 } else { 125 EXPECT_TRUE(tab.ShouldShowIcon()); 126 EXPECT_FALSE(tab.ShouldShowMediaIndicator()); 127 } 128 break; 129 default: 130 EXPECT_LE(3, tab.IconCapacity()); 131 EXPECT_TRUE(tab.ShouldShowIcon()); 132 if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) 133 EXPECT_TRUE(tab.ShouldShowMediaIndicator()); 134 else 135 EXPECT_FALSE(tab.ShouldShowMediaIndicator()); 136 break; 137 } 138 } else { // Tab not active and not mini tab. 139 switch (tab.IconCapacity()) { 140 case 0: 141 EXPECT_FALSE(tab.ShouldShowCloseBox()); 142 EXPECT_FALSE(tab.ShouldShowIcon()); 143 EXPECT_FALSE(tab.ShouldShowMediaIndicator()); 144 break; 145 case 1: 146 EXPECT_FALSE(tab.ShouldShowCloseBox()); 147 if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) { 148 EXPECT_FALSE(tab.ShouldShowIcon()); 149 EXPECT_TRUE(tab.ShouldShowMediaIndicator()); 150 } else { 151 EXPECT_TRUE(tab.ShouldShowIcon()); 152 EXPECT_FALSE(tab.ShouldShowMediaIndicator()); 153 } 154 break; 155 default: 156 EXPECT_LE(2, tab.IconCapacity()); 157 EXPECT_TRUE(tab.ShouldShowIcon()); 158 if (tab.data_.media_state != TAB_MEDIA_STATE_NONE) 159 EXPECT_TRUE(tab.ShouldShowMediaIndicator()); 160 else 161 EXPECT_FALSE(tab.ShouldShowMediaIndicator()); 162 break; 163 } 164 } 165 166 // Check positioning of elements with respect to each other, and that they 167 // are fully within the contents bounds. 168 const gfx::Rect contents_bounds = tab.GetContentsBounds(); 169 if (tab.ShouldShowIcon()) { 170 EXPECT_LE(contents_bounds.x(), tab.favicon_bounds_.x()); 171 if (tab.title_->width() > 0) 172 EXPECT_LE(tab.favicon_bounds_.right(), tab.title_->x()); 173 EXPECT_LE(contents_bounds.y(), tab.favicon_bounds_.y()); 174 EXPECT_LE(tab.favicon_bounds_.bottom(), contents_bounds.bottom()); 175 } 176 if (tab.ShouldShowIcon() && tab.ShouldShowMediaIndicator()) 177 EXPECT_LE(tab.favicon_bounds_.right(), GetMediaIndicatorBounds(tab).x()); 178 if (tab.ShouldShowMediaIndicator()) { 179 if (tab.title_->width() > 0) { 180 EXPECT_LE(tab.title_->bounds().right(), 181 GetMediaIndicatorBounds(tab).x()); 182 } 183 EXPECT_LE(GetMediaIndicatorBounds(tab).right(), contents_bounds.right()); 184 EXPECT_LE(contents_bounds.y(), GetMediaIndicatorBounds(tab).y()); 185 EXPECT_LE(GetMediaIndicatorBounds(tab).bottom(), 186 contents_bounds.bottom()); 187 } 188 if (tab.ShouldShowMediaIndicator() && tab.ShouldShowCloseBox()) { 189 // Note: The media indicator can overlap the left-insets of the close box, 190 // but should otherwise be to the left of the close button. 191 EXPECT_LE(GetMediaIndicatorBounds(tab).right(), 192 tab.close_button_->bounds().x() + 193 tab.close_button_->GetInsets().left()); 194 } 195 if (tab.ShouldShowCloseBox()) { 196 // Note: The title bounds can overlap the left-insets of the close box, 197 // but should otherwise be to the left of the close button. 198 if (tab.title_->width() > 0) { 199 EXPECT_LE(tab.title_->bounds().right(), 200 tab.close_button_->bounds().x() + 201 tab.close_button_->GetInsets().left()); 202 } 203 EXPECT_LE(tab.close_button_->bounds().right(), contents_bounds.right()); 204 EXPECT_LE(contents_bounds.y(), tab.close_button_->bounds().y()); 205 EXPECT_LE(tab.close_button_->bounds().bottom(), contents_bounds.bottom()); 206 } 207 } 208 209 private: 210 static gfx::Rect GetMediaIndicatorBounds(const Tab& tab) { 211 if (!tab.media_indicator_button_) { 212 ADD_FAILURE(); 213 return gfx::Rect(); 214 } 215 return tab.media_indicator_button_->bounds(); 216 } 217 218 std::string original_locale_; 219 }; 220 221 TEST_P(TabTest, HitTestTopPixel) { 222 if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { 223 LOG(WARNING) << "Testing of RTL locale not supported on current platform."; 224 return; 225 } 226 227 Widget widget; 228 Widget::InitParams params(CreateParams(Widget::InitParams::TYPE_WINDOW)); 229 params.ownership = Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; 230 params.bounds.SetRect(10, 20, 300, 400); 231 widget.Init(params); 232 233 FakeTabController tab_controller; 234 Tab tab(&tab_controller); 235 widget.GetContentsView()->AddChildView(&tab); 236 tab.SetBoundsRect(gfx::Rect(gfx::Point(0, 0), Tab::GetStandardSize())); 237 238 // Tabs have some shadow in the top, so by default we don't hit the tab there. 239 int middle_x = tab.width() / 2; 240 EXPECT_FALSE(tab.HitTestPoint(gfx::Point(middle_x, 0))); 241 242 // Tabs are slanted, so a click halfway down the left edge won't hit it. 243 int middle_y = tab.height() / 2; 244 EXPECT_FALSE(tab.HitTestPoint(gfx::Point(0, middle_y))); 245 246 // If the window is maximized, however, we want clicks in the top edge to 247 // select the tab. 248 widget.Maximize(); 249 EXPECT_TRUE(tab.HitTestPoint(gfx::Point(middle_x, 0))); 250 251 // But clicks in the area above the slanted sides should still miss. 252 EXPECT_FALSE(tab.HitTestPoint(gfx::Point(0, 0))); 253 EXPECT_FALSE(tab.HitTestPoint(gfx::Point(tab.width() - 1, 0))); 254 } 255 256 TEST_P(TabTest, LayoutAndVisibilityOfElements) { 257 if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { 258 LOG(WARNING) << "Testing of RTL locale not supported on current platform."; 259 return; 260 } 261 262 static const TabMediaState kMediaStatesToTest[] = { 263 TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING, 264 TAB_MEDIA_STATE_AUDIO_PLAYING, TAB_MEDIA_STATE_AUDIO_MUTING 265 }; 266 267 FakeTabController controller; 268 Tab tab(&controller); 269 270 SkBitmap bitmap; 271 bitmap.allocN32Pixels(16, 16); 272 TabRendererData data; 273 data.favicon = gfx::ImageSkia::CreateFrom1xBitmap(bitmap); 274 275 // Perform layout over all possible combinations, checking for correct 276 // results. 277 for (int is_mini_tab = 0; is_mini_tab < 2; ++is_mini_tab) { 278 for (int is_active_tab = 0; is_active_tab < 2; ++is_active_tab) { 279 for (size_t media_state_index = 0; 280 media_state_index < arraysize(kMediaStatesToTest); 281 ++media_state_index) { 282 const TabMediaState media_state = kMediaStatesToTest[media_state_index]; 283 SCOPED_TRACE(::testing::Message() 284 << (is_active_tab ? "Active" : "Inactive") << ' ' 285 << (is_mini_tab ? "Mini " : "") 286 << "Tab with media indicator state " << media_state); 287 288 data.mini = !!is_mini_tab; 289 controller.set_active_tab(!!is_active_tab); 290 data.media_state = media_state; 291 tab.SetData(data); 292 293 // Test layout for every width from standard to minimum. 294 gfx::Rect bounds(gfx::Point(0, 0), Tab::GetStandardSize()); 295 int min_width; 296 if (is_mini_tab) { 297 bounds.set_width(Tab::GetMiniWidth()); 298 min_width = Tab::GetMiniWidth(); 299 } else { 300 min_width = is_active_tab ? Tab::GetMinimumSelectedSize().width() : 301 Tab::GetMinimumUnselectedSize().width(); 302 } 303 while (bounds.width() >= min_width) { 304 SCOPED_TRACE(::testing::Message() << "bounds=" << bounds.ToString()); 305 tab.SetBoundsRect(bounds); // Invokes Tab::Layout(). 306 CheckForExpectedLayoutAndVisibilityOfElements(tab); 307 bounds.set_width(bounds.width() - 1); 308 } 309 } 310 } 311 } 312 } 313 314 // Regression test for http://crbug.com/226253. Calling Layout() more than once 315 // shouldn't change the insets of the close button. 316 TEST_P(TabTest, CloseButtonLayout) { 317 if (testing_for_rtl_locale() && !base::i18n::IsRTL()) { 318 LOG(WARNING) << "Testing of RTL locale not supported on current platform."; 319 return; 320 } 321 322 FakeTabController tab_controller; 323 Tab tab(&tab_controller); 324 tab.SetBounds(0, 0, 100, 50); 325 tab.Layout(); 326 gfx::Insets close_button_insets = tab.close_button_->GetInsets(); 327 tab.Layout(); 328 gfx::Insets close_button_insets_2 = tab.close_button_->GetInsets(); 329 EXPECT_EQ(close_button_insets.top(), close_button_insets_2.top()); 330 EXPECT_EQ(close_button_insets.left(), close_button_insets_2.left()); 331 EXPECT_EQ(close_button_insets.bottom(), close_button_insets_2.bottom()); 332 EXPECT_EQ(close_button_insets.right(), close_button_insets_2.right()); 333 334 // Also make sure the close button is sized as large as the tab. 335 EXPECT_EQ(50, tab.close_button_->bounds().height()); 336 } 337 338 // Test in both a LTR and a RTL locale. Note: The fact that the UI code is 339 // configured for an RTL locale does *not* change how the coordinates are 340 // examined in the tests above because views::View and friends are supposed to 341 // auto-mirror the widgets when painting. Thus, what we're testing here is that 342 // there's no code in Tab that will erroneously subvert this automatic 343 // coordinate translation. http://crbug.com/384179 344 INSTANTIATE_TEST_CASE_P(, TabTest, ::testing::Values(false, true)); 345