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/message_loop/message_loop.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
11 #import "chrome/browser/ui/cocoa/tabs/media_indicator_button_cocoa.h"
12 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
13 #import "chrome/browser/ui/cocoa/tabs/tab_controller_target.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_strip_drag_controller.h"
15 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
16 #include "testing/gtest/include/gtest/gtest.h"
17 #import "testing/gtest_mac.h"
18 #include "testing/platform_test.h"
19 #include "ui/base/resource/resource_bundle.h"
20 #include "ui/resources/grit/ui_resources.h"
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> {
28 base::scoped_nsobject<TabStripDragController> dragController_;
34 @implementation TabControllerTestTarget
36 if ((self = [super init])) {
37 dragController_.reset(
38 [[TabStripDragController alloc] initWithTabStripController:nil]);
48 - (void)selectTab:(id)sender {
51 - (void)closeTab:(id)sender {
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]
61 timestamp:[current timestamp]
62 windowNumber:[window windowNumber]
67 [window postEvent:up atStart:YES];
69 - (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
70 forController:(TabController*)controller {
72 - (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
73 forController:(TabController*)controller {
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"));
84 - (id<TabDraggingEventTarget>)dragController {
85 return dragController_.get();
91 CGFloat LeftMargin(NSRect superFrame, NSRect subFrame) {
92 return NSMinX(subFrame) - NSMinX(superFrame);
95 CGFloat RightMargin(NSRect superFrame, NSRect subFrame) {
96 return NSMaxX(superFrame) - NSMaxX(subFrame);
99 // The dragging code in TabView makes heavy use of autorelease pools so
100 // inherit from CocoaTest to have one created for us.
101 class TabControllerTest : public CocoaTest {
103 TabControllerTest() { }
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 mediaIndicatorButton] showingMediaState];
111 if ([controller pinned]) {
112 EXPECT_EQ(1, [controller iconCapacity]);
113 if (indicatorState != TAB_MEDIA_STATE_NONE) {
114 EXPECT_FALSE([controller shouldShowIcon]);
115 EXPECT_TRUE([controller shouldShowMediaIndicator]);
117 EXPECT_TRUE([controller shouldShowIcon]);
118 EXPECT_FALSE([controller shouldShowMediaIndicator]);
120 EXPECT_FALSE([controller shouldShowCloseButton]);
121 } else if ([controller selected]) {
122 EXPECT_TRUE([controller shouldShowCloseButton]);
123 switch ([controller iconCapacity]) {
126 EXPECT_FALSE([controller shouldShowIcon]);
127 EXPECT_FALSE([controller shouldShowMediaIndicator]);
130 if (indicatorState != TAB_MEDIA_STATE_NONE) {
131 EXPECT_FALSE([controller shouldShowIcon]);
132 EXPECT_TRUE([controller shouldShowMediaIndicator]);
134 EXPECT_TRUE([controller shouldShowIcon]);
135 EXPECT_FALSE([controller shouldShowMediaIndicator]);
139 EXPECT_LE(3, [controller iconCapacity]);
140 EXPECT_TRUE([controller shouldShowIcon]);
141 if (indicatorState != TAB_MEDIA_STATE_NONE)
142 EXPECT_TRUE([controller shouldShowMediaIndicator]);
144 EXPECT_FALSE([controller shouldShowMediaIndicator]);
147 } else { // Tab not selected/active and not pinned tab.
148 switch ([controller iconCapacity]) {
150 EXPECT_FALSE([controller shouldShowCloseButton]);
151 EXPECT_FALSE([controller shouldShowIcon]);
152 EXPECT_FALSE([controller shouldShowMediaIndicator]);
155 EXPECT_FALSE([controller shouldShowCloseButton]);
156 if (indicatorState != TAB_MEDIA_STATE_NONE) {
157 EXPECT_FALSE([controller shouldShowIcon]);
158 EXPECT_TRUE([controller shouldShowMediaIndicator]);
160 EXPECT_TRUE([controller shouldShowIcon]);
161 EXPECT_FALSE([controller shouldShowMediaIndicator]);
165 EXPECT_LE(2, [controller iconCapacity]);
166 EXPECT_TRUE([controller shouldShowIcon]);
167 if (indicatorState != TAB_MEDIA_STATE_NONE)
168 EXPECT_TRUE([controller shouldShowMediaIndicator]);
170 EXPECT_FALSE([controller shouldShowMediaIndicator]);
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 pinned] == [[controller tabView] titleHidden]);
179 EXPECT_TRUE([controller shouldShowMediaIndicator] ==
180 ![[controller mediaIndicatorButton] isHidden]);
181 EXPECT_TRUE([controller shouldShowCloseButton] !=
182 [[controller closeButton] isHidden]);
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));
196 if ([controller shouldShowIcon] && [controller shouldShowMediaIndicator]) {
197 EXPECT_LE(NSMaxX([[controller iconView] frame]),
198 NSMinX([[controller mediaIndicatorButton] frame]));
200 if ([controller shouldShowMediaIndicator]) {
201 const NSRect mediaIndicatorFrame =
202 [[controller mediaIndicatorButton] 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));
209 if ([controller shouldShowMediaIndicator] &&
210 [controller shouldShowCloseButton]) {
211 EXPECT_LE(NSMaxX([[controller mediaIndicatorButton] frame]),
212 NSMinX([[controller closeButton] frame]));
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));
225 base::MessageLoop message_loop_;
228 // Tests creating the controller, sticking it in a window, and removing it.
229 TEST_F(TabControllerTest, Creation) {
230 NSWindow* window = test_window();
231 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
232 [[window contentView] addSubview:[controller view]];
233 EXPECT_TRUE([controller tabView]);
234 EXPECT_EQ([[controller view] window], window);
235 [[controller view] display]; // Test drawing to ensure nothing leaks/crashes.
236 [[controller view] removeFromSuperview];
239 // Tests sending it a close message and ensuring that the target/action get
240 // called. Mimics the user clicking on the close button in the tab.
241 TEST_F(TabControllerTest, Close) {
242 NSWindow* window = test_window();
243 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
244 [[window contentView] addSubview:[controller view]];
246 base::scoped_nsobject<TabControllerTestTarget> target(
247 [[TabControllerTestTarget alloc] init]);
248 EXPECT_FALSE([target closed]);
249 [controller setTarget:target];
250 EXPECT_EQ(target.get(), [controller target]);
252 [controller closeTab:nil];
253 EXPECT_TRUE([target closed]);
255 [[controller view] removeFromSuperview];
258 // Tests setting the |selected| property via code.
259 TEST_F(TabControllerTest, APISelection) {
260 NSWindow* window = test_window();
261 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
262 [[window contentView] addSubview:[controller view]];
264 EXPECT_FALSE([controller selected]);
265 [controller setSelected:YES];
266 EXPECT_TRUE([controller selected]);
268 [[controller view] removeFromSuperview];
271 // Tests setting the |loading| property via code.
272 TEST_F(TabControllerTest, Loading) {
273 NSWindow* window = test_window();
274 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
275 [[window contentView] addSubview:[controller view]];
277 EXPECT_EQ(kTabDone, [controller loadingState]);
278 [controller setLoadingState:kTabWaiting];
279 EXPECT_EQ(kTabWaiting, [controller loadingState]);
280 [controller setLoadingState:kTabLoading];
281 EXPECT_EQ(kTabLoading, [controller loadingState]);
282 [controller setLoadingState:kTabDone];
283 EXPECT_EQ(kTabDone, [controller loadingState]);
285 [[controller view] removeFromSuperview];
288 // Tests selecting the tab with the mouse click and ensuring the target/action
290 TEST_F(TabControllerTest, UserSelection) {
291 NSWindow* window = test_window();
293 // Create a tab at a known location in the window that we can click on
294 // to activate selection.
295 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
296 [[window contentView] addSubview:[controller view]];
297 NSRect frame = [[controller view] frame];
298 frame.size.width = [TabController minTabWidth];
299 frame.origin = NSZeroPoint;
300 [[controller view] setFrame:frame];
302 // Set the target and action.
303 base::scoped_nsobject<TabControllerTestTarget> target(
304 [[TabControllerTestTarget alloc] init]);
305 EXPECT_FALSE([target selected]);
306 [controller setTarget:target];
307 [controller setAction:@selector(selectTab:)];
308 EXPECT_EQ(target.get(), [controller target]);
309 EXPECT_EQ(@selector(selectTab:), [controller action]);
311 // In order to track a click, we have to fake a mouse down and a mouse
312 // up, but the down goes into a tight drag loop. To break the loop, we have
313 // to fire a timer that sends a mouse up event while the "drag" is ongoing.
314 [NSTimer scheduledTimerWithTimeInterval:0.1
316 selector:@selector(mouseTimer:)
319 NSEvent* current = [NSApp currentEvent];
320 NSPoint click_point = NSMakePoint(frame.size.width / 2,
321 frame.size.height / 2);
322 NSEvent* down = [NSEvent mouseEventWithType:NSLeftMouseDown
325 timestamp:[current timestamp]
326 windowNumber:[window windowNumber]
331 [[controller view] mouseDown:down];
333 // Check our target was told the tab got selected.
334 EXPECT_TRUE([target selected]);
336 [[controller view] removeFromSuperview];
339 TEST_F(TabControllerTest, IconCapacity) {
340 NSWindow* window = test_window();
341 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
342 [[window contentView] addSubview:[controller view]];
343 int cap = [controller iconCapacity];
346 NSRect frame = [[controller view] frame];
347 frame.size.width += 500;
348 [[controller view] setFrame:frame];
349 int newcap = [controller iconCapacity];
350 EXPECT_GT(newcap, cap);
353 TEST_F(TabControllerTest, ShouldShowIcon) {
354 NSWindow* window = test_window();
355 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
356 [[window contentView] addSubview:[controller view]];
357 int cap = [controller iconCapacity];
360 // Tab is minimum width, both icon and close box should be hidden.
361 NSRect frame = [[controller view] frame];
362 frame.size.width = [TabController minTabWidth];
363 [[controller view] setFrame:frame];
364 EXPECT_FALSE([controller shouldShowIcon]);
365 EXPECT_FALSE([controller shouldShowCloseButton]);
367 // Setting the icon when tab is at min width should not show icon (bug 18359).
368 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
369 base::scoped_nsobject<NSImage> image(
370 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
371 [controller setIconImage:image];
372 NSView* newIcon = [controller iconView];
373 EXPECT_TRUE([newIcon isHidden]);
375 // Tab is at active minimum width. Since it's active, the close box
376 // should be visible.
377 [controller setActive:YES];
378 frame = [[controller view] frame];
379 frame.size.width = [TabController minActiveTabWidth];
380 [[controller view] setFrame:frame];
381 EXPECT_FALSE([controller shouldShowIcon]);
382 EXPECT_TRUE([newIcon isHidden]);
383 EXPECT_TRUE([controller shouldShowCloseButton]);
385 // Test expanding the tab to max width and ensure the icon and close box
386 // get put back, even when de-activated.
387 frame.size.width = [TabController maxTabWidth];
388 [[controller view] setFrame:frame];
389 EXPECT_TRUE([controller shouldShowIcon]);
390 EXPECT_FALSE([newIcon isHidden]);
391 EXPECT_TRUE([controller shouldShowCloseButton]);
392 [controller setActive:NO];
393 EXPECT_TRUE([controller shouldShowIcon]);
394 EXPECT_TRUE([controller shouldShowCloseButton]);
396 cap = [controller iconCapacity];
400 TEST_F(TabControllerTest, Menu) {
401 NSWindow* window = test_window();
402 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
403 base::scoped_nsobject<TabControllerTestTarget> target(
404 [[TabControllerTestTarget alloc] init]);
405 [controller setTarget:target];
407 [[window contentView] addSubview:[controller view]];
408 int cap = [controller iconCapacity];
411 // Asking the view for its menu should yield a valid menu.
412 NSMenu* menu = [[controller view] menu];
414 EXPECT_EQ(3, [menu numberOfItems]);
417 // Tests that the title field is correctly positioned and sized when the
419 TEST_F(TabControllerTest, TitleViewLayout) {
420 NSWindow* window = test_window();
422 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
423 [[window contentView] addSubview:[controller view]];
424 NSRect tabFrame = [[controller view] frame];
425 tabFrame.size.width = [TabController maxTabWidth];
426 [[controller view] setFrame:tabFrame];
428 const NSRect originalTabFrame = [[controller view] frame];
429 const NSRect originalIconFrame = [[controller iconView] frame];
430 const NSRect originalCloseFrame = [[controller closeButton] frame];
431 const NSRect originalTitleFrame = [[controller tabView] titleFrame];
433 // Sanity check the start state.
434 EXPECT_FALSE([[controller iconView] isHidden]);
435 EXPECT_FALSE([[controller closeButton] isHidden]);
436 EXPECT_GT(NSWidth([[controller view] frame]),
437 NSWidth([[controller tabView] titleFrame]));
439 // Resize the tab so that that the it shrinks.
440 tabFrame.size.width = [TabController minTabWidth];
441 [[controller view] setFrame:tabFrame];
443 // The icon view and close button should be hidden and the title view should
444 // be resize to take up their space.
445 EXPECT_TRUE([[controller iconView] isHidden]);
446 EXPECT_TRUE([[controller closeButton] isHidden]);
447 EXPECT_GT(NSWidth([[controller view] frame]),
448 NSWidth([[controller tabView] titleFrame]));
449 EXPECT_EQ(LeftMargin(originalTabFrame, originalIconFrame),
450 LeftMargin([[controller view] frame],
451 [[controller tabView] titleFrame]));
452 EXPECT_EQ(RightMargin(originalTabFrame, originalCloseFrame),
453 RightMargin([[controller view] frame],
454 [[controller tabView] titleFrame]));
456 // Resize the tab so that that the it grows.
457 tabFrame.size.width = static_cast<int>([TabController maxTabWidth] * 0.75);
458 [[controller view] setFrame:tabFrame];
460 // The icon view and close button should be visible again and the title view
461 // should be resized to make room for them.
462 EXPECT_FALSE([[controller iconView] isHidden]);
463 EXPECT_FALSE([[controller closeButton] isHidden]);
464 EXPECT_GT(NSWidth([[controller view] frame]),
465 NSWidth([[controller tabView] titleFrame]));
466 EXPECT_EQ(LeftMargin(originalTabFrame, originalTitleFrame),
467 LeftMargin([[controller view] frame],
468 [[controller tabView] titleFrame]));
469 EXPECT_EQ(RightMargin(originalTabFrame, originalTitleFrame),
470 RightMargin([[controller view] frame],
471 [[controller tabView] titleFrame]));
474 // A comprehensive test of the layout and visibility of all elements (favicon,
475 // throbber indicators, titile text, media indicator button, and close button)
476 // over all relevant combinations of tab state. This test overlaps with parts
477 // of the other tests above.
478 // Flaky: https://code.google.com/p/chromium/issues/detail?id=311668
479 TEST_F(TabControllerTest, DISABLED_LayoutAndVisibilityOfSubviews) {
480 static const TabMediaState kMediaStatesToTest[] = {
481 TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING,
482 TAB_MEDIA_STATE_AUDIO_PLAYING, TAB_MEDIA_STATE_AUDIO_MUTING
485 NSWindow* const window = test_window();
487 // Create TabController instance and place its view into the test window.
488 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
489 [[window contentView] addSubview:[controller view]];
492 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
493 base::scoped_nsobject<NSImage> favicon(
494 rb.GetNativeImageNamed(IDR_DEFAULT_FAVICON).CopyNSImage());
496 // Trigger TabController to auto-create the MediaIndicatorButton.
497 [controller setMediaState:TAB_MEDIA_STATE_AUDIO_PLAYING];
498 [controller setMediaState:TAB_MEDIA_STATE_NONE];
499 base::scoped_nsobject<MediaIndicatorButton> mediaIndicatorButton(
500 [[controller mediaIndicatorButton] retain]);
501 ASSERT_TRUE(mediaIndicatorButton.get());
503 // Perform layout over all possible combinations, checking for correct
505 for (int isPinnedTab = 0; isPinnedTab < 2; ++isPinnedTab) {
506 for (int isActiveTab = 0; isActiveTab < 2; ++isActiveTab) {
507 for (size_t mediaStateIndex = 0;
508 mediaStateIndex < arraysize(kMediaStatesToTest);
510 const TabMediaState mediaState = kMediaStatesToTest[mediaStateIndex];
511 SCOPED_TRACE(::testing::Message()
512 << (isActiveTab ? "Active" : "Inactive") << ' '
513 << (isPinnedTab ? "Pinned " : "")
514 << "Tab with media indicator state " << mediaState);
516 // Simulate what tab_strip_controller would do to set up the
517 // TabController state.
518 [controller setPinned:(isPinnedTab ? YES : NO)];
519 [controller setActive:(isActiveTab ? YES : NO)];
520 [controller setIconImage:favicon];
521 [controller setMediaState:mediaState];
522 [controller updateVisibility];
524 // Test layout for every width from maximum to minimum.
525 NSRect tabFrame = [[controller view] frame];
528 tabFrame.size.width = minWidth = [TabController pinnedTabWidth];
530 tabFrame.size.width = [TabController maxTabWidth];
531 minWidth = isActiveTab ? [TabController minActiveTabWidth] :
532 [TabController minTabWidth];
534 while (NSWidth(tabFrame) >= minWidth) {
535 SCOPED_TRACE(::testing::Message() << "width=" << tabFrame.size.width);
536 [[controller view] setFrame:tabFrame];
537 CheckForExpectedLayoutAndVisibilityOfSubviews(controller);
538 --tabFrame.size.width;