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_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 #include "grit/theme_resources.h"
15 #include "grit/ui_resources.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"
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 // Helper to create an NSImageView that contains an image fetched from
99 // ui::ResourceBundle.
100 NSImageView* CreateImageViewFromResourceBundle(int resource_id) {
101 ui::ResourceBundle& rb = ui::ResourceBundle::GetSharedInstance();
102 NSImage* const image = rb.GetNativeImageNamed(resource_id).ToNSImage();
105 frame.size = [image size];
106 NSImageView* const view = [[NSImageView alloc] initWithFrame:frame];
107 [view setImage:image];
111 // The dragging code in TabView makes heavy use of autorelease pools so
112 // inherit from CocoaTest to have one created for us.
113 class TabControllerTest : public CocoaTest {
115 TabControllerTest() { }
117 static void CheckForExpectedLayoutAndVisibilityOfSubviews(
118 const TabController* controller) {
119 // Check whether subviews should be visible when they are supposed to be,
120 // given Tab size and TabRendererData state.
121 const TabMediaState indicatorState =
122 [[controller mediaIndicatorView] mediaState];
123 if ([controller mini]) {
124 EXPECT_EQ(1, [controller iconCapacity]);
125 if (indicatorState != TAB_MEDIA_STATE_NONE) {
126 EXPECT_FALSE([controller shouldShowIcon]);
127 EXPECT_TRUE([controller shouldShowMediaIndicator]);
129 EXPECT_TRUE([controller shouldShowIcon]);
130 EXPECT_FALSE([controller shouldShowMediaIndicator]);
132 EXPECT_FALSE([controller shouldShowCloseButton]);
133 } else if ([controller selected]) {
134 EXPECT_TRUE([controller shouldShowCloseButton]);
135 switch ([controller iconCapacity]) {
138 EXPECT_FALSE([controller shouldShowIcon]);
139 EXPECT_FALSE([controller shouldShowMediaIndicator]);
142 if (indicatorState != TAB_MEDIA_STATE_NONE) {
143 EXPECT_FALSE([controller shouldShowIcon]);
144 EXPECT_TRUE([controller shouldShowMediaIndicator]);
146 EXPECT_TRUE([controller shouldShowIcon]);
147 EXPECT_FALSE([controller shouldShowMediaIndicator]);
151 EXPECT_LE(3, [controller iconCapacity]);
152 EXPECT_TRUE([controller shouldShowIcon]);
153 if (indicatorState != TAB_MEDIA_STATE_NONE)
154 EXPECT_TRUE([controller shouldShowMediaIndicator]);
156 EXPECT_FALSE([controller shouldShowMediaIndicator]);
159 } else { // Tab not selected/active and not mini tab.
160 switch ([controller iconCapacity]) {
162 EXPECT_FALSE([controller shouldShowCloseButton]);
163 EXPECT_FALSE([controller shouldShowIcon]);
164 EXPECT_FALSE([controller shouldShowMediaIndicator]);
167 EXPECT_FALSE([controller shouldShowCloseButton]);
168 if (indicatorState != TAB_MEDIA_STATE_NONE) {
169 EXPECT_FALSE([controller shouldShowIcon]);
170 EXPECT_TRUE([controller shouldShowMediaIndicator]);
172 EXPECT_TRUE([controller shouldShowIcon]);
173 EXPECT_FALSE([controller shouldShowMediaIndicator]);
177 EXPECT_LE(2, [controller iconCapacity]);
178 EXPECT_TRUE([controller shouldShowIcon]);
179 if (indicatorState != TAB_MEDIA_STATE_NONE)
180 EXPECT_TRUE([controller shouldShowMediaIndicator]);
182 EXPECT_FALSE([controller shouldShowMediaIndicator]);
187 // Make sure the NSView's "isHidden" state jives with the "shouldShowXXX."
188 EXPECT_TRUE([controller shouldShowIcon] ==
189 (!![controller iconView] && ![[controller iconView] isHidden]));
190 EXPECT_TRUE([controller mini] == [[controller titleView] isHidden]);
191 EXPECT_TRUE([controller shouldShowMediaIndicator] ==
192 ![[controller mediaIndicatorView] isHidden]);
193 EXPECT_TRUE([controller shouldShowCloseButton] !=
194 [[controller closeButton] isHidden]);
196 // Check positioning of elements with respect to each other, and that they
197 // are fully within the tab frame.
198 const NSRect tabFrame = [[controller view] frame];
199 const NSRect titleFrame = [[controller titleView] frame];
200 if ([controller shouldShowIcon]) {
201 const NSRect iconFrame = [[controller iconView] frame];
202 EXPECT_LE(NSMinX(tabFrame), NSMinX(iconFrame));
203 if (NSWidth(titleFrame) > 0)
204 EXPECT_LE(NSMaxX(iconFrame), NSMinX(titleFrame));
205 EXPECT_LE(NSMinY(tabFrame), NSMinY(iconFrame));
206 EXPECT_LE(NSMaxY(iconFrame), NSMaxY(tabFrame));
208 if ([controller shouldShowIcon] && [controller shouldShowMediaIndicator]) {
209 EXPECT_LE(NSMaxX([[controller iconView] frame]),
210 NSMinX([[controller mediaIndicatorView] frame]));
212 if ([controller shouldShowMediaIndicator]) {
213 const NSRect mediaIndicatorFrame =
214 [[controller mediaIndicatorView] frame];
215 if (NSWidth(titleFrame) > 0)
216 EXPECT_LE(NSMaxX(titleFrame), NSMinX(mediaIndicatorFrame));
217 EXPECT_LE(NSMaxX(mediaIndicatorFrame), NSMaxX(tabFrame));
218 EXPECT_LE(NSMinY(tabFrame), NSMinY(mediaIndicatorFrame));
219 EXPECT_LE(NSMaxY(mediaIndicatorFrame), NSMaxY(tabFrame));
221 if ([controller shouldShowMediaIndicator] &&
222 [controller shouldShowCloseButton]) {
223 EXPECT_LE(NSMaxX([[controller mediaIndicatorView] frame]),
224 NSMinX([[controller closeButton] frame]));
226 if ([controller shouldShowCloseButton]) {
227 const NSRect closeButtonFrame = [[controller closeButton] frame];
228 if (NSWidth(titleFrame) > 0)
229 EXPECT_LE(NSMaxX(titleFrame), NSMinX(closeButtonFrame));
230 EXPECT_LE(NSMaxX(closeButtonFrame), NSMaxX(tabFrame));
231 EXPECT_LE(NSMinY(tabFrame), NSMinY(closeButtonFrame));
232 EXPECT_LE(NSMaxY(closeButtonFrame), NSMaxY(tabFrame));
237 // Tests creating the controller, sticking it in a window, and removing it.
238 TEST_F(TabControllerTest, Creation) {
239 NSWindow* window = test_window();
240 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
241 [[window contentView] addSubview:[controller view]];
242 EXPECT_TRUE([controller tabView]);
243 EXPECT_EQ([[controller view] window], window);
244 [[controller view] display]; // Test drawing to ensure nothing leaks/crashes.
245 [[controller view] removeFromSuperview];
248 // Tests sending it a close message and ensuring that the target/action get
249 // called. Mimics the user clicking on the close button in the tab.
250 TEST_F(TabControllerTest, Close) {
251 NSWindow* window = test_window();
252 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
253 [[window contentView] addSubview:[controller view]];
255 base::scoped_nsobject<TabControllerTestTarget> target(
256 [[TabControllerTestTarget alloc] init]);
257 EXPECT_FALSE([target closed]);
258 [controller setTarget:target];
259 EXPECT_EQ(target.get(), [controller target]);
261 [controller closeTab:nil];
262 EXPECT_TRUE([target closed]);
264 [[controller view] removeFromSuperview];
267 // Tests setting the |selected| property via code.
268 TEST_F(TabControllerTest, APISelection) {
269 NSWindow* window = test_window();
270 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
271 [[window contentView] addSubview:[controller view]];
273 EXPECT_FALSE([controller selected]);
274 [controller setSelected:YES];
275 EXPECT_TRUE([controller selected]);
277 [[controller view] removeFromSuperview];
280 // Tests setting the |loading| property via code.
281 TEST_F(TabControllerTest, Loading) {
282 NSWindow* window = test_window();
283 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
284 [[window contentView] addSubview:[controller view]];
286 EXPECT_EQ(kTabDone, [controller loadingState]);
287 [controller setLoadingState:kTabWaiting];
288 EXPECT_EQ(kTabWaiting, [controller loadingState]);
289 [controller setLoadingState:kTabLoading];
290 EXPECT_EQ(kTabLoading, [controller loadingState]);
291 [controller setLoadingState:kTabDone];
292 EXPECT_EQ(kTabDone, [controller loadingState]);
294 [[controller view] removeFromSuperview];
297 // Tests selecting the tab with the mouse click and ensuring the target/action
299 TEST_F(TabControllerTest, UserSelection) {
300 NSWindow* window = test_window();
302 // Create a tab at a known location in the window that we can click on
303 // to activate selection.
304 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
305 [[window contentView] addSubview:[controller view]];
306 NSRect frame = [[controller view] frame];
307 frame.size.width = [TabController minTabWidth];
308 frame.origin = NSZeroPoint;
309 [[controller view] setFrame:frame];
311 // Set the target and action.
312 base::scoped_nsobject<TabControllerTestTarget> target(
313 [[TabControllerTestTarget alloc] init]);
314 EXPECT_FALSE([target selected]);
315 [controller setTarget:target];
316 [controller setAction:@selector(selectTab:)];
317 EXPECT_EQ(target.get(), [controller target]);
318 EXPECT_EQ(@selector(selectTab:), [controller action]);
320 // In order to track a click, we have to fake a mouse down and a mouse
321 // up, but the down goes into a tight drag loop. To break the loop, we have
322 // to fire a timer that sends a mouse up event while the "drag" is ongoing.
323 [NSTimer scheduledTimerWithTimeInterval:0.1
325 selector:@selector(mouseTimer:)
328 NSEvent* current = [NSApp currentEvent];
329 NSPoint click_point = NSMakePoint(frame.size.width / 2,
330 frame.size.height / 2);
331 NSEvent* down = [NSEvent mouseEventWithType:NSLeftMouseDown
334 timestamp:[current timestamp]
335 windowNumber:[window windowNumber]
340 [[controller view] mouseDown:down];
342 // Check our target was told the tab got selected.
343 EXPECT_TRUE([target selected]);
345 [[controller view] removeFromSuperview];
348 TEST_F(TabControllerTest, IconCapacity) {
349 NSWindow* window = test_window();
350 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
351 [[window contentView] addSubview:[controller view]];
352 int cap = [controller iconCapacity];
355 NSRect frame = [[controller view] frame];
356 frame.size.width += 500;
357 [[controller view] setFrame:frame];
358 int newcap = [controller iconCapacity];
359 EXPECT_GT(newcap, cap);
362 TEST_F(TabControllerTest, ShouldShowIcon) {
363 NSWindow* window = test_window();
364 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
365 [[window contentView] addSubview:[controller view]];
366 int cap = [controller iconCapacity];
369 // Tab is minimum width, both icon and close box should be hidden.
370 NSRect frame = [[controller view] frame];
371 frame.size.width = [TabController minTabWidth];
372 [[controller view] setFrame:frame];
373 EXPECT_FALSE([controller shouldShowIcon]);
374 EXPECT_FALSE([controller shouldShowCloseButton]);
376 // Setting the icon when tab is at min width should not show icon (bug 18359).
377 base::scoped_nsobject<NSView> newIcon(
378 [[NSView alloc] initWithFrame:NSMakeRect(0, 0, 16, 16)]);
379 [controller setIconView:newIcon.get()];
380 EXPECT_TRUE([newIcon isHidden]);
382 // Tab is at selected minimum width. Since it's selected, the close box
383 // should be visible.
384 [controller setSelected:YES];
385 frame = [[controller view] frame];
386 frame.size.width = [TabController minSelectedTabWidth];
387 [[controller view] setFrame:frame];
388 EXPECT_FALSE([controller shouldShowIcon]);
389 EXPECT_TRUE([newIcon isHidden]);
390 EXPECT_TRUE([controller shouldShowCloseButton]);
392 // Test expanding the tab to max width and ensure the icon and close box
393 // get put back, even when de-selected.
394 frame.size.width = [TabController maxTabWidth];
395 [[controller view] setFrame:frame];
396 EXPECT_TRUE([controller shouldShowIcon]);
397 EXPECT_FALSE([newIcon isHidden]);
398 EXPECT_TRUE([controller shouldShowCloseButton]);
399 [controller setSelected:NO];
400 EXPECT_TRUE([controller shouldShowIcon]);
401 EXPECT_TRUE([controller shouldShowCloseButton]);
403 cap = [controller iconCapacity];
407 TEST_F(TabControllerTest, Menu) {
408 NSWindow* window = test_window();
409 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
410 base::scoped_nsobject<TabControllerTestTarget> target(
411 [[TabControllerTestTarget alloc] init]);
412 [controller setTarget:target];
414 [[window contentView] addSubview:[controller view]];
415 int cap = [controller iconCapacity];
418 // Asking the view for its menu should yield a valid menu.
419 NSMenu* menu = [[controller view] menu];
421 EXPECT_EQ(3, [menu numberOfItems]);
424 // Tests that the title field is correctly positioned and sized when the
426 TEST_F(TabControllerTest, TitleViewLayout) {
427 NSWindow* window = test_window();
429 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
430 [[window contentView] addSubview:[controller view]];
431 NSRect tabFrame = [[controller view] frame];
432 tabFrame.size.width = [TabController maxTabWidth];
433 [[controller view] setFrame:tabFrame];
435 const NSRect originalTabFrame = [[controller view] frame];
436 const NSRect originalIconFrame = [[controller iconView] frame];
437 const NSRect originalCloseFrame = [[controller closeButton] frame];
438 const NSRect originalTitleFrame = [[controller titleView] frame];
440 // Sanity check the start state.
441 EXPECT_FALSE([[controller iconView] isHidden]);
442 EXPECT_FALSE([[controller closeButton] isHidden]);
443 EXPECT_GT(NSWidth([[controller view] frame]),
444 NSWidth([[controller titleView] frame]));
446 // Resize the tab so that that the it shrinks.
447 tabFrame.size.width = [TabController minTabWidth];
448 [[controller view] setFrame:tabFrame];
450 // The icon view and close button should be hidden and the title view should
451 // be resize to take up their space.
452 EXPECT_TRUE([[controller iconView] isHidden]);
453 EXPECT_TRUE([[controller closeButton] isHidden]);
454 EXPECT_GT(NSWidth([[controller view] frame]),
455 NSWidth([[controller titleView] frame]));
456 EXPECT_EQ(LeftMargin(originalTabFrame, originalIconFrame),
457 LeftMargin([[controller view] frame],
458 [[controller titleView] frame]));
459 EXPECT_EQ(RightMargin(originalTabFrame, originalCloseFrame),
460 RightMargin([[controller view] frame],
461 [[controller titleView] frame]));
463 // Resize the tab so that that the it grows.
464 tabFrame.size.width = static_cast<int>([TabController maxTabWidth] * 0.75);
465 [[controller view] setFrame:tabFrame];
467 // The icon view and close button should be visible again and the title view
468 // should be resized to make room for them.
469 EXPECT_FALSE([[controller iconView] isHidden]);
470 EXPECT_FALSE([[controller closeButton] isHidden]);
471 EXPECT_GT(NSWidth([[controller view] frame]),
472 NSWidth([[controller titleView] frame]));
473 EXPECT_EQ(LeftMargin(originalTabFrame, originalTitleFrame),
474 LeftMargin([[controller view] frame],
475 [[controller titleView] frame]));
476 EXPECT_EQ(RightMargin(originalTabFrame, originalTitleFrame),
477 RightMargin([[controller view] frame],
478 [[controller titleView] frame]));
481 // A comprehensive test of the layout and visibility of all elements (favicon,
482 // throbber indicators, titile text, audio indicator, and close button) over all
483 // relevant combinations of tab state. This test overlaps with parts of the
484 // other tests above.
485 // Flaky: https://code.google.com/p/chromium/issues/detail?id=311668
486 TEST_F(TabControllerTest, DISABLED_LayoutAndVisibilityOfSubviews) {
487 static const TabMediaState kMediaStatesToTest[] = {
488 TAB_MEDIA_STATE_NONE, TAB_MEDIA_STATE_CAPTURING,
489 TAB_MEDIA_STATE_AUDIO_PLAYING
492 NSWindow* const window = test_window();
494 // Create TabController instance and place its view into the test window.
495 base::scoped_nsobject<TabController> controller([[TabController alloc] init]);
496 [[window contentView] addSubview:[controller view]];
498 // Create favicon and media indicator views. Disable animation in the media
499 // indicator view so that TabController's "what should be shown" logic can be
500 // tested effectively. If animations were left enabled, the
501 // shouldShowMediaIndicator method would return true during fade-out
503 base::scoped_nsobject<NSImageView> faviconView(
504 CreateImageViewFromResourceBundle(IDR_DEFAULT_FAVICON));
505 base::scoped_nsobject<MediaIndicatorView> mediaIndicatorView(
506 [[MediaIndicatorView alloc] init]);
507 [mediaIndicatorView disableAnimations];
508 [controller setMediaIndicatorView:mediaIndicatorView];
510 // Perform layout over all possible combinations, checking for correct
512 for (int isMiniTab = 0; isMiniTab < 2; ++isMiniTab) {
513 for (int isActiveTab = 0; isActiveTab < 2; ++isActiveTab) {
514 for (size_t mediaStateIndex = 0;
515 mediaStateIndex < arraysize(kMediaStatesToTest);
517 const TabMediaState mediaState = kMediaStatesToTest[mediaStateIndex];
518 SCOPED_TRACE(::testing::Message()
519 << (isActiveTab ? "Active" : "Inactive") << ' '
520 << (isMiniTab ? "Mini " : "")
521 << "Tab with media indicator state " << mediaState);
523 // Simulate what tab_strip_controller would do to set up the
524 // TabController state.
525 [controller setMini:(isMiniTab ? YES : NO)];
526 [controller setActive:(isActiveTab ? YES : NO)];
527 [[controller mediaIndicatorView] updateIndicator:mediaState];
528 [controller setIconView:faviconView];
530 // Test layout for every width from maximum to minimum.
531 NSRect tabFrame = [[controller view] frame];
534 tabFrame.size.width = minWidth = [TabController miniTabWidth];
536 tabFrame.size.width = [TabController maxTabWidth];
537 minWidth = isActiveTab ? [TabController minSelectedTabWidth] :
538 [TabController minTabWidth];
540 while (NSWidth(tabFrame) >= minWidth) {
541 SCOPED_TRACE(::testing::Message() << "width=" << tabFrame.size.width);
542 [[controller view] setFrame:tabFrame];
543 CheckForExpectedLayoutAndVisibilityOfSubviews(controller);
544 --tabFrame.size.width;