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.
5 #import <Cocoa/Cocoa.h>
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_button_cocoa.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 "testing/gtest/include/gtest/gtest.h"
16 #import "testing/gtest_mac.h"
17 #include "testing/platform_test.h"
18 #include "ui/base/resource/resource_bundle.h"
19 #include "ui/resources/grit/ui_resources.h"
21 // Implements the target interface for the tab, which gets sent messages when
22 // the tab is clicked on by the user and when its close box is clicked.
23 @interface TabControllerTestTarget : NSObject<TabControllerTarget> {
27 base::scoped_nsobject<TabStripDragController> dragController_;
33 @implementation TabControllerTestTarget
35 if ((self = [super init])) {
36 dragController_.reset(
37 [[TabStripDragController alloc] initWithTabStripController:nil]);
47 - (void)selectTab:(id)sender {
50 - (void)closeTab:(id)sender {
53 - (void)mouseTimer:(NSTimer*)timer {
54 // Fire the mouseUp to break the TabView drag loop.
55 NSEvent* current = [NSApp currentEvent];
56 NSWindow* window = [timer userInfo];
57 NSEvent* up = [NSEvent mouseEventWithType:NSLeftMouseUp
58 location:[current locationInWindow]
60 timestamp:[current timestamp]
61 windowNumber:[window windowNumber]
66 [window postEvent:up atStart:YES];
68 - (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
69 forController:(TabController*)controller {
71 - (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
72 forController:(TabController*)controller {
75 - (ui::SimpleMenuModel*)contextMenuModelForController:(TabController*)controller
76 menuDelegate:(ui::SimpleMenuModel::Delegate*)delegate {
77 ui::SimpleMenuModel* model = new ui::SimpleMenuModel(delegate);
78 model->AddItem(1, base::ASCIIToUTF16("Hello World"));
79 model->AddItem(2, base::ASCIIToUTF16("Allays"));
80 model->AddItem(3, base::ASCIIToUTF16("Chromium"));
83 - (id<TabDraggingEventTarget>)dragController {
84 return dragController_.get();
90 CGFloat LeftMargin(NSRect superFrame, NSRect subFrame) {
91 return NSMinX(subFrame) - NSMinX(superFrame);
94 CGFloat RightMargin(NSRect superFrame, NSRect subFrame) {
95 return NSMaxX(superFrame) - NSMaxX(subFrame);
98 // The dragging code in TabView makes heavy use of autorelease pools so
99 // inherit from CocoaTest to have one created for us.
100 class TabControllerTest : public CocoaTest {
102 TabControllerTest() { }
104 static void CheckForExpectedLayoutAndVisibilityOfSubviews(
105 const TabController* controller) {
106 // Check whether subviews should be visible when they are supposed to be,
107 // given Tab size and TabRendererData state.
108 const TabMediaState indicatorState =
109 [[controller mediaIndicatorButton] showingMediaState];
110 if ([controller mini]) {
111 EXPECT_EQ(1, [controller iconCapacity]);
112 if (indicatorState != TAB_MEDIA_STATE_NONE) {
113 EXPECT_FALSE([controller shouldShowIcon]);
114 EXPECT_TRUE([controller shouldShowMediaIndicator]);
116 EXPECT_TRUE([controller shouldShowIcon]);
117 EXPECT_FALSE([controller shouldShowMediaIndicator]);
119 EXPECT_FALSE([controller shouldShowCloseButton]);
120 } else if ([controller selected]) {
121 EXPECT_TRUE([controller shouldShowCloseButton]);
122 switch ([controller iconCapacity]) {
125 EXPECT_FALSE([controller shouldShowIcon]);
126 EXPECT_FALSE([controller shouldShowMediaIndicator]);
129 if (indicatorState != TAB_MEDIA_STATE_NONE) {
130 EXPECT_FALSE([controller shouldShowIcon]);
131 EXPECT_TRUE([controller shouldShowMediaIndicator]);
133 EXPECT_TRUE([controller shouldShowIcon]);
134 EXPECT_FALSE([controller shouldShowMediaIndicator]);
138 EXPECT_LE(3, [controller iconCapacity]);
139 EXPECT_TRUE([controller shouldShowIcon]);
140 if (indicatorState != TAB_MEDIA_STATE_NONE)
141 EXPECT_TRUE([controller shouldShowMediaIndicator]);
143 EXPECT_FALSE([controller shouldShowMediaIndicator]);
146 } else { // Tab not selected/active and not mini tab.
147 switch ([controller iconCapacity]) {
149 EXPECT_FALSE([controller shouldShowCloseButton]);
150 EXPECT_FALSE([controller shouldShowIcon]);
151 EXPECT_FALSE([controller shouldShowMediaIndicator]);
154 EXPECT_FALSE([controller shouldShowCloseButton]);
155 if (indicatorState != TAB_MEDIA_STATE_NONE) {
156 EXPECT_FALSE([controller shouldShowIcon]);
157 EXPECT_TRUE([controller shouldShowMediaIndicator]);
159 EXPECT_TRUE([controller shouldShowIcon]);
160 EXPECT_FALSE([controller shouldShowMediaIndicator]);
164 EXPECT_LE(2, [controller iconCapacity]);
165 EXPECT_TRUE([controller shouldShowIcon]);
166 if (indicatorState != TAB_MEDIA_STATE_NONE)
167 EXPECT_TRUE([controller shouldShowMediaIndicator]);
169 EXPECT_FALSE([controller shouldShowMediaIndicator]);
174 // Make sure the NSView's "isHidden" state jives with the "shouldShowXXX."
175 EXPECT_TRUE([controller shouldShowIcon] ==
176 (!![controller iconView] && ![[controller iconView] isHidden]));
177 EXPECT_TRUE([controller mini] == [[controller tabView] titleHidden]);
178 EXPECT_TRUE([controller shouldShowMediaIndicator] ==
179 ![[controller mediaIndicatorButton] isHidden]);
180 EXPECT_TRUE([controller shouldShowCloseButton] !=
181 [[controller closeButton] isHidden]);
183 // Check positioning of elements with respect to each other, and that they
184 // are fully within the tab frame.
185 const NSRect tabFrame = [[controller view] frame];
186 const NSRect titleFrame = [[controller tabView] titleFrame];
187 if ([controller shouldShowIcon]) {
188 const NSRect iconFrame = [[controller iconView] frame];
189 EXPECT_LE(NSMinX(tabFrame), NSMinX(iconFrame));
190 if (NSWidth(titleFrame) > 0)
191 EXPECT_LE(NSMaxX(iconFrame), NSMinX(titleFrame));
192 EXPECT_LE(NSMinY(tabFrame), NSMinY(iconFrame));
193 EXPECT_LE(NSMaxY(iconFrame), NSMaxY(tabFrame));
195 if ([controller shouldShowIcon] && [controller shouldShowMediaIndicator]) {
196 EXPECT_LE(NSMaxX([[controller iconView] frame]),
197 NSMinX([[controller mediaIndicatorButton] frame]));
199 if ([controller shouldShowMediaIndicator]) {
200 const NSRect mediaIndicatorFrame =
201 [[controller mediaIndicatorButton] frame];
202 if (NSWidth(titleFrame) > 0)
203 EXPECT_LE(NSMaxX(titleFrame), NSMinX(mediaIndicatorFrame));
204 EXPECT_LE(NSMaxX(mediaIndicatorFrame), NSMaxX(tabFrame));
205 EXPECT_LE(NSMinY(tabFrame), NSMinY(mediaIndicatorFrame));
206 EXPECT_LE(NSMaxY(mediaIndicatorFrame), NSMaxY(tabFrame));
208 if ([controller shouldShowMediaIndicator] &&
209 [controller shouldShowCloseButton]) {
210 EXPECT_LE(NSMaxX([[controller mediaIndicatorButton] frame]),
211 NSMinX([[controller closeButton] frame]));
213 if ([controller shouldShowCloseButton]) {
214 const NSRect closeButtonFrame = [[controller closeButton] frame];
215 if (NSWidth(titleFrame) > 0)
216 EXPECT_LE(NSMaxX(titleFrame), NSMinX(closeButtonFrame));
217 EXPECT_LE(NSMaxX(closeButtonFrame), NSMaxX(tabFrame));
218 EXPECT_LE(NSMinY(tabFrame), NSMinY(closeButtonFrame));
219 EXPECT_LE(NSMaxY(closeButtonFrame), NSMaxY(tabFrame));
224 // Tests creating the controller, sticking it in a window, and removing it.
225 TEST_F(TabControllerTest, Creation) {
226 NSWindow* window = test_window();
227 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
228 [[window contentView] addSubview:[controller view]];
229 EXPECT_TRUE([controller tabView]);
230 EXPECT_EQ([[controller view] window], window);
231 [[controller view] display]; // Test drawing to ensure nothing leaks/crashes.
232 [[controller view] removeFromSuperview];
235 // Tests sending it a close message and ensuring that the target/action get
236 // called. Mimics the user clicking on the close button in the tab.
237 TEST_F(TabControllerTest, Close) {
238 NSWindow* window = test_window();
239 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
240 [[window contentView] addSubview:[controller view]];
242 base::scoped_nsobject<TabControllerTestTarget> target(
243 [[TabControllerTestTarget alloc] init]);
244 EXPECT_FALSE([target closed]);
245 [controller setTarget:target];
246 EXPECT_EQ(target.get(), [controller target]);
248 [controller closeTab:nil];
249 EXPECT_TRUE([target closed]);
251 [[controller view] removeFromSuperview];
254 // Tests setting the |selected| property via code.
255 TEST_F(TabControllerTest, APISelection) {
256 NSWindow* window = test_window();
257 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
258 [[window contentView] addSubview:[controller view]];
260 EXPECT_FALSE([controller selected]);
261 [controller setSelected:YES];
262 EXPECT_TRUE([controller selected]);
264 [[controller view] removeFromSuperview];
267 // Tests setting the |loading| property via code.
268 TEST_F(TabControllerTest, Loading) {
269 NSWindow* window = test_window();
270 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
271 [[window contentView] addSubview:[controller view]];
273 EXPECT_EQ(kTabDone, [controller loadingState]);
274 [controller setLoadingState:kTabWaiting];
275 EXPECT_EQ(kTabWaiting, [controller loadingState]);
276 [controller setLoadingState:kTabLoading];
277 EXPECT_EQ(kTabLoading, [controller loadingState]);
278 [controller setLoadingState:kTabDone];
279 EXPECT_EQ(kTabDone, [controller loadingState]);
281 [[controller view] removeFromSuperview];
284 // Tests selecting the tab with the mouse click and ensuring the target/action
286 TEST_F(TabControllerTest, UserSelection) {
287 NSWindow* window = test_window();
289 // Create a tab at a known location in the window that we can click on
290 // to activate selection.
291 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
292 [[window contentView] addSubview:[controller view]];
293 NSRect frame = [[controller view] frame];
294 frame.size.width = [TabController minTabWidth];
295 frame.origin = NSZeroPoint;
296 [[controller view] setFrame:frame];
298 // Set the target and action.
299 base::scoped_nsobject<TabControllerTestTarget> target(
300 [[TabControllerTestTarget alloc] init]);
301 EXPECT_FALSE([target selected]);
302 [controller setTarget:target];
303 [controller setAction:@selector(selectTab:)];
304 EXPECT_EQ(target.get(), [controller target]);
305 EXPECT_EQ(@selector(selectTab:), [controller action]);
307 // In order to track a click, we have to fake a mouse down and a mouse
308 // up, but the down goes into a tight drag loop. To break the loop, we have
309 // to fire a timer that sends a mouse up event while the "drag" is ongoing.
310 [NSTimer scheduledTimerWithTimeInterval:0.1
312 selector:@selector(mouseTimer:)
315 NSEvent* current = [NSApp currentEvent];
316 NSPoint click_point = NSMakePoint(frame.size.width / 2,
317 frame.size.height / 2);
318 NSEvent* down = [NSEvent mouseEventWithType:NSLeftMouseDown
321 timestamp:[current timestamp]
322 windowNumber:[window windowNumber]
327 [[controller view] mouseDown:down];
329 // Check our target was told the tab got selected.
330 EXPECT_TRUE([target selected]);
332 [[controller view] removeFromSuperview];
335 TEST_F(TabControllerTest, IconCapacity) {
336 NSWindow* window = test_window();
337 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
338 [[window contentView] addSubview:[controller view]];
339 int cap = [controller iconCapacity];
342 NSRect frame = [[controller view] frame];
343 frame.size.width += 500;
344 [[controller view] setFrame:frame];
345 int newcap = [controller iconCapacity];
346 EXPECT_GT(newcap, cap);
349 TEST_F(TabControllerTest, ShouldShowIcon) {
350 NSWindow* window = test_window();
351 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
352 [[window contentView] addSubview:[controller view]];
353 int cap = [controller iconCapacity];
356 // Tab is minimum width, both icon and close box should be hidden.
357 NSRect frame = [[controller view] frame];
358 frame.size.width = [TabController minTabWidth];
359 [[controller view] setFrame:frame];
360 EXPECT_FALSE([controller shouldShowIcon]);
361 EXPECT_FALSE([controller shouldShowCloseButton]);
363 // Setting the icon when tab is at min width should not show icon (bug 18359).
364 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
365 base::scoped_nsobject<NSImage> image(
366 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
367 [controller setIconImage:image];
368 NSView* newIcon = [controller iconView];
369 EXPECT_TRUE([newIcon isHidden]);
371 // Tab is at active minimum width. Since it's active, the close box
372 // should be visible.
373 [controller setActive:YES];
374 frame = [[controller view] frame];
375 frame.size.width = [TabController minActiveTabWidth];
376 [[controller view] setFrame:frame];
377 EXPECT_FALSE([controller shouldShowIcon]);
378 EXPECT_TRUE([newIcon isHidden]);
379 EXPECT_TRUE([controller shouldShowCloseButton]);
381 // Test expanding the tab to max width and ensure the icon and close box
382 // get put back, even when de-activated.
383 frame.size.width = [TabController maxTabWidth];
384 [[controller view] setFrame:frame];
385 EXPECT_TRUE([controller shouldShowIcon]);
386 EXPECT_FALSE([newIcon isHidden]);
387 EXPECT_TRUE([controller shouldShowCloseButton]);
388 [controller setActive:NO];
389 EXPECT_TRUE([controller shouldShowIcon]);
390 EXPECT_TRUE([controller shouldShowCloseButton]);
392 cap = [controller iconCapacity];
396 TEST_F(TabControllerTest, Menu) {
397 NSWindow* window = test_window();
398 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
399 base::scoped_nsobject<TabControllerTestTarget> target(
400 [[TabControllerTestTarget alloc] init]);
401 [controller setTarget:target];
403 [[window contentView] addSubview:[controller view]];
404 int cap = [controller iconCapacity];
407 // Asking the view for its menu should yield a valid menu.
408 NSMenu* menu = [[controller view] menu];
410 EXPECT_EQ(3, [menu numberOfItems]);
413 // Tests that the title field is correctly positioned and sized when the
415 TEST_F(TabControllerTest, TitleViewLayout) {
416 NSWindow* window = test_window();
418 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
419 [[window contentView] addSubview:[controller view]];
420 NSRect tabFrame = [[controller view] frame];
421 tabFrame.size.width = [TabController maxTabWidth];
422 [[controller view] setFrame:tabFrame];
424 const NSRect originalTabFrame = [[controller view] frame];
425 const NSRect originalIconFrame = [[controller iconView] frame];
426 const NSRect originalCloseFrame = [[controller closeButton] frame];
427 const NSRect originalTitleFrame = [[controller tabView] titleFrame];
429 // Sanity check the start state.
430 EXPECT_FALSE([[controller iconView] isHidden]);
431 EXPECT_FALSE([[controller closeButton] isHidden]);
432 EXPECT_GT(NSWidth([[controller view] frame]),
433 NSWidth([[controller tabView] titleFrame]));
435 // Resize the tab so that that the it shrinks.
436 tabFrame.size.width = [TabController minTabWidth];
437 [[controller view] setFrame:tabFrame];
439 // The icon view and close button should be hidden and the title view should
440 // be resize to take up their space.
441 EXPECT_TRUE([[controller iconView] isHidden]);
442 EXPECT_TRUE([[controller closeButton] isHidden]);
443 EXPECT_GT(NSWidth([[controller view] frame]),
444 NSWidth([[controller tabView] titleFrame]));
445 EXPECT_EQ(LeftMargin(originalTabFrame, originalIconFrame),
446 LeftMargin([[controller view] frame],
447 [[controller tabView] titleFrame]));
448 EXPECT_EQ(RightMargin(originalTabFrame, originalCloseFrame),
449 RightMargin([[controller view] frame],
450 [[controller tabView] titleFrame]));
452 // Resize the tab so that that the it grows.
453 tabFrame.size.width = static_cast<int>([TabController maxTabWidth] * 0.75);
454 [[controller view] setFrame:tabFrame];
456 // The icon view and close button should be visible again and the title view
457 // should be resized to make room for them.
458 EXPECT_FALSE([[controller iconView] isHidden]);
459 EXPECT_FALSE([[controller closeButton] isHidden]);
460 EXPECT_GT(NSWidth([[controller view] frame]),
461 NSWidth([[controller tabView] titleFrame]));
462 EXPECT_EQ(LeftMargin(originalTabFrame, originalTitleFrame),
463 LeftMargin([[controller view] frame],
464 [[controller tabView] titleFrame]));
465 EXPECT_EQ(RightMargin(originalTabFrame, originalTitleFrame),
466 RightMargin([[controller view] frame],
467 [[controller tabView] titleFrame]));
470 // A comprehensive test of the layout and visibility of all elements (favicon,
471 // throbber indicators, titile text, media indicator button, and close button)
472 // over all relevant combinations of tab state. This test overlaps with parts
473 // of the other tests above.
474 // Flaky: https://code.google.com/p/chromium/issues/detail?id=311668
475 TEST_F(TabControllerTest, DISABLED_LayoutAndVisibilityOfSubviews) {
476 static const TabMediaState kMediaStatesToTest[] = {
477 TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING,
478 TAB_MEDIA_STATE_AUDIO_PLAYING, TAB_MEDIA_STATE_AUDIO_MUTING
481 NSWindow* const window = test_window();
483 // Create TabController instance and place its view into the test window.
484 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
485 [[window contentView] addSubview:[controller view]];
488 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
489 base::scoped_nsobject<NSImage> favicon(
490 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
492 // Trigger TabController to auto-create the MediaIndicatorButton.
493 [controller setMediaState:TAB_MEDIA_STATE_AUDIO_PLAYING];
494 [controller setMediaState:TAB_MEDIA_STATE_NONE];
495 base::scoped_nsobject<MediaIndicatorButton> mediaIndicatorButton(
496 [[controller mediaIndicatorButton] retain]);
497 ASSERT_TRUE(mediaIndicatorButton.get());
499 // Perform layout over all possible combinations, checking for correct
501 for (int isMiniTab = 0; isMiniTab < 2; ++isMiniTab) {
502 for (int isActiveTab = 0; isActiveTab < 2; ++isActiveTab) {
503 for (size_t mediaStateIndex = 0;
504 mediaStateIndex < arraysize(kMediaStatesToTest);
506 const TabMediaState mediaState = kMediaStatesToTest[mediaStateIndex];
507 SCOPED_TRACE(::testing::Message()
508 << (isActiveTab ? "Active" : "Inactive") << ' '
509 << (isMiniTab ? "Mini " : "")
510 << "Tab with media indicator state " << mediaState);
512 // Simulate what tab_strip_controller would do to set up the
513 // TabController state.
514 [controller setMini:(isMiniTab ? YES : NO)];
515 [controller setActive:(isActiveTab ? YES : NO)];
516 [controller setIconImage:favicon];
517 [controller setMediaState:mediaState];
518 [controller updateVisibility];
520 // Test layout for every width from maximum to minimum.
521 NSRect tabFrame = [[controller view] frame];
524 tabFrame.size.width = minWidth = [TabController miniTabWidth];
526 tabFrame.size.width = [TabController maxTabWidth];
527 minWidth = isActiveTab ? [TabController minActiveTabWidth] :
528 [TabController minTabWidth];
530 while (NSWidth(tabFrame) >= minWidth) {
531 SCOPED_TRACE(::testing::Message() << "width=" << tabFrame.size.width);
532 [[controller view] setFrame:tabFrame];
533 CheckForExpectedLayoutAndVisibilityOfSubviews(controller);
534 --tabFrame.size.width;