tab_controller_unittest.mm revision a1401311d1ab56c4ed0a474bd38c108f75cb0cd9
1// Copyright (c) 2011 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#import <Cocoa/Cocoa.h> 6 7#import "base/mac/scoped_nsobject.h" 8#include "base/strings/utf_string_conversions.h" 9#include "chrome/browser/ui/cocoa/cocoa_test_helper.h" 10#import "chrome/browser/ui/cocoa/tabs/media_indicator_view.h" 11#import "chrome/browser/ui/cocoa/tabs/tab_controller.h" 12#import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h" 13#import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h" 14#import "chrome/browser/ui/cocoa/tabs/tab_view.h" 15#include "grit/theme_resources.h" 16#include "grit/ui_resources.h" 17#include "testing/gtest/include/gtest/gtest.h" 18#import "testing/gtest_mac.h" 19#include "testing/platform_test.h" 20#include "ui/base/resource/resource_bundle.h" 21 22// Implements the target interface for the tab, which gets sent messages when 23// the tab is clicked on by the user and when its close box is clicked. 24@interface TabControllerTestTarget : NSObject<TabControllerTarget> { 25 @private 26 bool selected_; 27 bool closed_; 28 base::scoped_nsobject<TabStripDragController> dragController_; 29} 30- (bool)selected; 31- (bool)closed; 32@end 33 34@implementation TabControllerTestTarget 35- (id)init { 36 if ((self = [super init])) { 37 dragController_.reset( 38 [[TabStripDragController alloc] initWithTabStripController:nil]); 39 } 40 return self; 41} 42- (bool)selected { 43 return selected_; 44} 45- (bool)closed { 46 return closed_; 47} 48- (void)selectTab:(id)sender { 49 selected_ = true; 50} 51- (void)closeTab:(id)sender { 52 closed_ = true; 53} 54- (void)mouseTimer:(NSTimer*)timer { 55 // Fire the mouseUp to break the TabView drag loop. 56 NSEvent* current = [NSApp currentEvent]; 57 NSWindow* window = [timer userInfo]; 58 NSEvent* up = [NSEvent mouseEventWithType:NSLeftMouseUp 59 location:[current locationInWindow] 60 modifierFlags:0 61 timestamp:[current timestamp] 62 windowNumber:[window windowNumber] 63 context:nil 64 eventNumber:0 65 clickCount:1 66 pressure:1.0]; 67 [window postEvent:up atStart:YES]; 68} 69- (void)commandDispatch:(TabStripModel::ContextMenuCommand)command 70 forController:(TabController*)controller { 71} 72- (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command 73 forController:(TabController*)controller { 74 return NO; 75} 76- (ui::SimpleMenuModel*)contextMenuModelForController:(TabController*)controller 77 menuDelegate:(ui::SimpleMenuModel::Delegate*)delegate { 78 ui::SimpleMenuModel* model = new ui::SimpleMenuModel(delegate); 79 model->AddItem(1, base::ASCIIToUTF16("Hello World")); 80 model->AddItem(2, base::ASCIIToUTF16("Allays")); 81 model->AddItem(3, base::ASCIIToUTF16("Chromium")); 82 return model; 83} 84- (id<TabDraggingEventTarget>)dragController { 85 return dragController_.get(); 86} 87@end 88 89namespace { 90 91CGFloat LeftMargin(NSRect superFrame, NSRect subFrame) { 92 return NSMinX(subFrame) - NSMinX(superFrame); 93} 94 95CGFloat RightMargin(NSRect superFrame, NSRect subFrame) { 96 return NSMaxX(superFrame) - NSMaxX(subFrame); 97} 98 99// The dragging code in TabView makes heavy use of autorelease pools so 100// inherit from CocoaTest to have one created for us. 101class TabControllerTest : public CocoaTest { 102 public: 103 TabControllerTest() { } 104 105 static void CheckForExpectedLayoutAndVisibilityOfSubviews( 106 const TabController* controller) { 107 // Check whether subviews should be visible when they are supposed to be, 108 // given Tab size and TabRendererData state. 109 const TabMediaState indicatorState = 110 [[controller mediaIndicatorView] mediaState]; 111 if ([controller mini]) { 112 EXPECT_EQ(1, [controller iconCapacity]); 113 if (indicatorState != TAB_MEDIA_STATE_NONE) { 114 EXPECT_FALSE([controller shouldShowIcon]); 115 EXPECT_TRUE([controller shouldShowMediaIndicator]); 116 } else { 117 EXPECT_TRUE([controller shouldShowIcon]); 118 EXPECT_FALSE([controller shouldShowMediaIndicator]); 119 } 120 EXPECT_FALSE([controller shouldShowCloseButton]); 121 } else if ([controller selected]) { 122 EXPECT_TRUE([controller shouldShowCloseButton]); 123 switch ([controller iconCapacity]) { 124 case 0: 125 case 1: 126 EXPECT_FALSE([controller shouldShowIcon]); 127 EXPECT_FALSE([controller shouldShowMediaIndicator]); 128 break; 129 case 2: 130 if (indicatorState != TAB_MEDIA_STATE_NONE) { 131 EXPECT_FALSE([controller shouldShowIcon]); 132 EXPECT_TRUE([controller shouldShowMediaIndicator]); 133 } else { 134 EXPECT_TRUE([controller shouldShowIcon]); 135 EXPECT_FALSE([controller shouldShowMediaIndicator]); 136 } 137 break; 138 default: 139 EXPECT_LE(3, [controller iconCapacity]); 140 EXPECT_TRUE([controller shouldShowIcon]); 141 if (indicatorState != TAB_MEDIA_STATE_NONE) 142 EXPECT_TRUE([controller shouldShowMediaIndicator]); 143 else 144 EXPECT_FALSE([controller shouldShowMediaIndicator]); 145 break; 146 } 147 } else { // Tab not selected/active and not mini tab. 148 switch ([controller iconCapacity]) { 149 case 0: 150 EXPECT_FALSE([controller shouldShowCloseButton]); 151 EXPECT_FALSE([controller shouldShowIcon]); 152 EXPECT_FALSE([controller shouldShowMediaIndicator]); 153 break; 154 case 1: 155 EXPECT_FALSE([controller shouldShowCloseButton]); 156 if (indicatorState != TAB_MEDIA_STATE_NONE) { 157 EXPECT_FALSE([controller shouldShowIcon]); 158 EXPECT_TRUE([controller shouldShowMediaIndicator]); 159 } else { 160 EXPECT_TRUE([controller shouldShowIcon]); 161 EXPECT_FALSE([controller shouldShowMediaIndicator]); 162 } 163 break; 164 default: 165 EXPECT_LE(2, [controller iconCapacity]); 166 EXPECT_TRUE([controller shouldShowIcon]); 167 if (indicatorState != TAB_MEDIA_STATE_NONE) 168 EXPECT_TRUE([controller shouldShowMediaIndicator]); 169 else 170 EXPECT_FALSE([controller shouldShowMediaIndicator]); 171 break; 172 } 173 } 174 175 // Make sure the NSView's "isHidden" state jives with the "shouldShowXXX." 176 EXPECT_TRUE([controller shouldShowIcon] == 177 (!![controller iconView] && ![[controller iconView] isHidden])); 178 EXPECT_TRUE([controller mini] == [[controller tabView] titleHidden]); 179 EXPECT_TRUE([controller shouldShowMediaIndicator] == 180 ![[controller mediaIndicatorView] isHidden]); 181 EXPECT_TRUE([controller shouldShowCloseButton] != 182 [[controller closeButton] isHidden]); 183 184 // Check positioning of elements with respect to each other, and that they 185 // are fully within the tab frame. 186 const NSRect tabFrame = [[controller view] frame]; 187 const NSRect titleFrame = [[controller tabView] titleFrame]; 188 if ([controller shouldShowIcon]) { 189 const NSRect iconFrame = [[controller iconView] frame]; 190 EXPECT_LE(NSMinX(tabFrame), NSMinX(iconFrame)); 191 if (NSWidth(titleFrame) > 0) 192 EXPECT_LE(NSMaxX(iconFrame), NSMinX(titleFrame)); 193 EXPECT_LE(NSMinY(tabFrame), NSMinY(iconFrame)); 194 EXPECT_LE(NSMaxY(iconFrame), NSMaxY(tabFrame)); 195 } 196 if ([controller shouldShowIcon] && [controller shouldShowMediaIndicator]) { 197 EXPECT_LE(NSMaxX([[controller iconView] frame]), 198 NSMinX([[controller mediaIndicatorView] frame])); 199 } 200 if ([controller shouldShowMediaIndicator]) { 201 const NSRect mediaIndicatorFrame = 202 [[controller mediaIndicatorView] frame]; 203 if (NSWidth(titleFrame) > 0) 204 EXPECT_LE(NSMaxX(titleFrame), NSMinX(mediaIndicatorFrame)); 205 EXPECT_LE(NSMaxX(mediaIndicatorFrame), NSMaxX(tabFrame)); 206 EXPECT_LE(NSMinY(tabFrame), NSMinY(mediaIndicatorFrame)); 207 EXPECT_LE(NSMaxY(mediaIndicatorFrame), NSMaxY(tabFrame)); 208 } 209 if ([controller shouldShowMediaIndicator] && 210 [controller shouldShowCloseButton]) { 211 EXPECT_LE(NSMaxX([[controller mediaIndicatorView] frame]), 212 NSMinX([[controller closeButton] frame])); 213 } 214 if ([controller shouldShowCloseButton]) { 215 const NSRect closeButtonFrame = [[controller closeButton] frame]; 216 if (NSWidth(titleFrame) > 0) 217 EXPECT_LE(NSMaxX(titleFrame), NSMinX(closeButtonFrame)); 218 EXPECT_LE(NSMaxX(closeButtonFrame), NSMaxX(tabFrame)); 219 EXPECT_LE(NSMinY(tabFrame), NSMinY(closeButtonFrame)); 220 EXPECT_LE(NSMaxY(closeButtonFrame), NSMaxY(tabFrame)); 221 } 222 } 223}; 224 225// Tests creating the controller, sticking it in a window, and removing it. 226TEST_F(TabControllerTest, Creation) { 227 NSWindow* window = test_window(); 228 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 229 [[window contentView] addSubview:[controller view]]; 230 EXPECT_TRUE([controller tabView]); 231 EXPECT_EQ([[controller view] window], window); 232 [[controller view] display]; // Test drawing to ensure nothing leaks/crashes. 233 [[controller view] removeFromSuperview]; 234} 235 236// Tests sending it a close message and ensuring that the target/action get 237// called. Mimics the user clicking on the close button in the tab. 238TEST_F(TabControllerTest, Close) { 239 NSWindow* window = test_window(); 240 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 241 [[window contentView] addSubview:[controller view]]; 242 243 base::scoped_nsobject<TabControllerTestTarget> target( 244 [[TabControllerTestTarget alloc] init]); 245 EXPECT_FALSE([target closed]); 246 [controller setTarget:target]; 247 EXPECT_EQ(target.get(), [controller target]); 248 249 [controller closeTab:nil]; 250 EXPECT_TRUE([target closed]); 251 252 [[controller view] removeFromSuperview]; 253} 254 255// Tests setting the |selected| property via code. 256TEST_F(TabControllerTest, APISelection) { 257 NSWindow* window = test_window(); 258 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 259 [[window contentView] addSubview:[controller view]]; 260 261 EXPECT_FALSE([controller selected]); 262 [controller setSelected:YES]; 263 EXPECT_TRUE([controller selected]); 264 265 [[controller view] removeFromSuperview]; 266} 267 268// Tests setting the |loading| property via code. 269TEST_F(TabControllerTest, Loading) { 270 NSWindow* window = test_window(); 271 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 272 [[window contentView] addSubview:[controller view]]; 273 274 EXPECT_EQ(kTabDone, [controller loadingState]); 275 [controller setLoadingState:kTabWaiting]; 276 EXPECT_EQ(kTabWaiting, [controller loadingState]); 277 [controller setLoadingState:kTabLoading]; 278 EXPECT_EQ(kTabLoading, [controller loadingState]); 279 [controller setLoadingState:kTabDone]; 280 EXPECT_EQ(kTabDone, [controller loadingState]); 281 282 [[controller view] removeFromSuperview]; 283} 284 285// Tests selecting the tab with the mouse click and ensuring the target/action 286// get called. 287TEST_F(TabControllerTest, UserSelection) { 288 NSWindow* window = test_window(); 289 290 // Create a tab at a known location in the window that we can click on 291 // to activate selection. 292 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 293 [[window contentView] addSubview:[controller view]]; 294 NSRect frame = [[controller view] frame]; 295 frame.size.width = [TabController minTabWidth]; 296 frame.origin = NSZeroPoint; 297 [[controller view] setFrame:frame]; 298 299 // Set the target and action. 300 base::scoped_nsobject<TabControllerTestTarget> target( 301 [[TabControllerTestTarget alloc] init]); 302 EXPECT_FALSE([target selected]); 303 [controller setTarget:target]; 304 [controller setAction:@selector(selectTab:)]; 305 EXPECT_EQ(target.get(), [controller target]); 306 EXPECT_EQ(@selector(selectTab:), [controller action]); 307 308 // In order to track a click, we have to fake a mouse down and a mouse 309 // up, but the down goes into a tight drag loop. To break the loop, we have 310 // to fire a timer that sends a mouse up event while the "drag" is ongoing. 311 [NSTimer scheduledTimerWithTimeInterval:0.1 312 target:target.get() 313 selector:@selector(mouseTimer:) 314 userInfo:window 315 repeats:NO]; 316 NSEvent* current = [NSApp currentEvent]; 317 NSPoint click_point = NSMakePoint(frame.size.width / 2, 318 frame.size.height / 2); 319 NSEvent* down = [NSEvent mouseEventWithType:NSLeftMouseDown 320 location:click_point 321 modifierFlags:0 322 timestamp:[current timestamp] 323 windowNumber:[window windowNumber] 324 context:nil 325 eventNumber:0 326 clickCount:1 327 pressure:1.0]; 328 [[controller view] mouseDown:down]; 329 330 // Check our target was told the tab got selected. 331 EXPECT_TRUE([target selected]); 332 333 [[controller view] removeFromSuperview]; 334} 335 336TEST_F(TabControllerTest, IconCapacity) { 337 NSWindow* window = test_window(); 338 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 339 [[window contentView] addSubview:[controller view]]; 340 int cap = [controller iconCapacity]; 341 EXPECT_GE(cap, 1); 342 343 NSRect frame = [[controller view] frame]; 344 frame.size.width += 500; 345 [[controller view] setFrame:frame]; 346 int newcap = [controller iconCapacity]; 347 EXPECT_GT(newcap, cap); 348} 349 350TEST_F(TabControllerTest, ShouldShowIcon) { 351 NSWindow* window = test_window(); 352 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 353 [[window contentView] addSubview:[controller view]]; 354 int cap = [controller iconCapacity]; 355 EXPECT_GT(cap, 0); 356 357 // Tab is minimum width, both icon and close box should be hidden. 358 NSRect frame = [[controller view] frame]; 359 frame.size.width = [TabController minTabWidth]; 360 [[controller view] setFrame:frame]; 361 EXPECT_FALSE([controller shouldShowIcon]); 362 EXPECT_FALSE([controller shouldShowCloseButton]); 363 364 // Setting the icon when tab is at min width should not show icon (bug 18359). 365 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 366 base::scoped_nsobject<NSImage> image( 367 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage()); 368 [controller setIconImage:image]; 369 NSView* newIcon = [controller iconView]; 370 EXPECT_TRUE([newIcon isHidden]); 371 372 // Tab is at selected minimum width. Since it's selected, the close box 373 // should be visible. 374 [controller setSelected:YES]; 375 frame = [[controller view] frame]; 376 frame.size.width = [TabController minSelectedTabWidth]; 377 [[controller view] setFrame:frame]; 378 EXPECT_FALSE([controller shouldShowIcon]); 379 EXPECT_TRUE([newIcon isHidden]); 380 EXPECT_TRUE([controller shouldShowCloseButton]); 381 382 // Test expanding the tab to max width and ensure the icon and close box 383 // get put back, even when de-selected. 384 frame.size.width = [TabController maxTabWidth]; 385 [[controller view] setFrame:frame]; 386 EXPECT_TRUE([controller shouldShowIcon]); 387 EXPECT_FALSE([newIcon isHidden]); 388 EXPECT_TRUE([controller shouldShowCloseButton]); 389 [controller setSelected:NO]; 390 EXPECT_TRUE([controller shouldShowIcon]); 391 EXPECT_TRUE([controller shouldShowCloseButton]); 392 393 cap = [controller iconCapacity]; 394 EXPECT_GT(cap, 0); 395} 396 397TEST_F(TabControllerTest, Menu) { 398 NSWindow* window = test_window(); 399 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 400 base::scoped_nsobject<TabControllerTestTarget> target( 401 [[TabControllerTestTarget alloc] init]); 402 [controller setTarget:target]; 403 404 [[window contentView] addSubview:[controller view]]; 405 int cap = [controller iconCapacity]; 406 EXPECT_GT(cap, 0); 407 408 // Asking the view for its menu should yield a valid menu. 409 NSMenu* menu = [[controller view] menu]; 410 EXPECT_TRUE(menu); 411 EXPECT_EQ(3, [menu numberOfItems]); 412} 413 414// Tests that the title field is correctly positioned and sized when the 415// view is resized. 416TEST_F(TabControllerTest, TitleViewLayout) { 417 NSWindow* window = test_window(); 418 419 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 420 [[window contentView] addSubview:[controller view]]; 421 NSRect tabFrame = [[controller view] frame]; 422 tabFrame.size.width = [TabController maxTabWidth]; 423 [[controller view] setFrame:tabFrame]; 424 425 const NSRect originalTabFrame = [[controller view] frame]; 426 const NSRect originalIconFrame = [[controller iconView] frame]; 427 const NSRect originalCloseFrame = [[controller closeButton] frame]; 428 const NSRect originalTitleFrame = [[controller tabView] titleFrame]; 429 430 // Sanity check the start state. 431 EXPECT_FALSE([[controller iconView] isHidden]); 432 EXPECT_FALSE([[controller closeButton] isHidden]); 433 EXPECT_GT(NSWidth([[controller view] frame]), 434 NSWidth([[controller tabView] titleFrame])); 435 436 // Resize the tab so that that the it shrinks. 437 tabFrame.size.width = [TabController minTabWidth]; 438 [[controller view] setFrame:tabFrame]; 439 440 // The icon view and close button should be hidden and the title view should 441 // be resize to take up their space. 442 EXPECT_TRUE([[controller iconView] isHidden]); 443 EXPECT_TRUE([[controller closeButton] isHidden]); 444 EXPECT_GT(NSWidth([[controller view] frame]), 445 NSWidth([[controller tabView] titleFrame])); 446 EXPECT_EQ(LeftMargin(originalTabFrame, originalIconFrame), 447 LeftMargin([[controller view] frame], 448 [[controller tabView] titleFrame])); 449 EXPECT_EQ(RightMargin(originalTabFrame, originalCloseFrame), 450 RightMargin([[controller view] frame], 451 [[controller tabView] titleFrame])); 452 453 // Resize the tab so that that the it grows. 454 tabFrame.size.width = static_cast<int>([TabController maxTabWidth] * 0.75); 455 [[controller view] setFrame:tabFrame]; 456 457 // The icon view and close button should be visible again and the title view 458 // should be resized to make room for them. 459 EXPECT_FALSE([[controller iconView] isHidden]); 460 EXPECT_FALSE([[controller closeButton] isHidden]); 461 EXPECT_GT(NSWidth([[controller view] frame]), 462 NSWidth([[controller tabView] titleFrame])); 463 EXPECT_EQ(LeftMargin(originalTabFrame, originalTitleFrame), 464 LeftMargin([[controller view] frame], 465 [[controller tabView] titleFrame])); 466 EXPECT_EQ(RightMargin(originalTabFrame, originalTitleFrame), 467 RightMargin([[controller view] frame], 468 [[controller tabView] titleFrame])); 469} 470 471// A comprehensive test of the layout and visibility of all elements (favicon, 472// throbber indicators, titile text, audio indicator, and close button) over all 473// relevant combinations of tab state. This test overlaps with parts of the 474// other tests above. 475// Flaky: https://code.google.com/p/chromium/issues/detail?id=311668 476TEST_F(TabControllerTest, DISABLED_LayoutAndVisibilityOfSubviews) { 477 static const TabMediaState kMediaStatesToTest[] = { 478 TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING, 479 TAB_MEDIA_STATE_AUDIO_PLAYING 480 }; 481 482 NSWindow* const window = test_window(); 483 484 // Create TabController instance and place its view into the test window. 485 base::scoped_nsobject<TabController> controller([[TabController alloc] init]); 486 [[window contentView] addSubview:[controller view]]; 487 488 // Create favicon and media indicator views. Disable animation in the media 489 // indicator view so that TabController's "what should be shown" logic can be 490 // tested effectively. If animations were left enabled, the 491 // shouldShowMediaIndicator method would return true during fade-out 492 // transitions. 493 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance(); 494 base::scoped_nsobject<NSImage> favicon( 495 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage()); 496 base::scoped_nsobject<MediaIndicatorView> mediaIndicatorView( 497 [[MediaIndicatorView alloc] init]); 498 [mediaIndicatorView disableAnimations]; 499 [controller setMediaIndicatorView:mediaIndicatorView]; 500 501 // Perform layout over all possible combinations, checking for correct 502 // results. 503 for (int isMiniTab = 0; isMiniTab < 2; ++isMiniTab) { 504 for (int isActiveTab = 0; isActiveTab < 2; ++isActiveTab) { 505 for (size_t mediaStateIndex = 0; 506 mediaStateIndex < arraysize(kMediaStatesToTest); 507 ++mediaStateIndex) { 508 const TabMediaState mediaState = kMediaStatesToTest[mediaStateIndex]; 509 SCOPED_TRACE(::testing::Message() 510 << (isActiveTab ? "Active" : "Inactive") << ' ' 511 << (isMiniTab ? "Mini " : "") 512 << "Tab with media indicator state " << mediaState); 513 514 // Simulate what tab_strip_controller would do to set up the 515 // TabController state. 516 [controller setMini:(isMiniTab ? YES : NO)]; 517 [controller setActive:(isActiveTab ? YES : NO)]; 518 [[controller mediaIndicatorView] updateIndicator:mediaState]; 519 [controller setIconImage:favicon]; 520 521 // Test layout for every width from maximum to minimum. 522 NSRect tabFrame = [[controller view] frame]; 523 int minWidth; 524 if (isMiniTab) { 525 tabFrame.size.width = minWidth = [TabController miniTabWidth]; 526 } else { 527 tabFrame.size.width = [TabController maxTabWidth]; 528 minWidth = isActiveTab ? [TabController minSelectedTabWidth] : 529 [TabController minTabWidth]; 530 } 531 while (NSWidth(tabFrame) >= minWidth) { 532 SCOPED_TRACE(::testing::Message() << "width=" << tabFrame.size.width); 533 [[controller view] setFrame:tabFrame]; 534 CheckForExpectedLayoutAndVisibilityOfSubviews(controller); 535 --tabFrame.size.width; 536 } 537 } 538 } 539 } 540} 541 542} // namespace 543