Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / ui / cocoa / tabs / tab_controller_unittest.mm
blob7b1909a3e7ee468c49566088673e27468492b1aa
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> {
25  @private
26   bool selected_;
27   bool closed_;
28   base::scoped_nsobject<TabStripDragController> dragController_;
30 - (bool)selected;
31 - (bool)closed;
32 @end
34 @implementation TabControllerTestTarget
35 - (id)init {
36   if ((self = [super init])) {
37     dragController_.reset(
38         [[TabStripDragController alloc] initWithTabStripController:nil]);
39   }
40   return self;
42 - (bool)selected {
43   return selected_;
45 - (bool)closed {
46   return closed_;
48 - (void)selectTab:(id)sender {
49   selected_ = true;
51 - (void)closeTab:(id)sender {
52   closed_ = true;
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];
69 - (void)commandDispatch:(TabStripModel::ContextMenuCommand)command
70           forController:(TabController*)controller {
72 - (BOOL)isCommandEnabled:(TabStripModel::ContextMenuCommand)command
73            forController:(TabController*)controller {
74   return NO;
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;
84 - (id<TabDraggingEventTarget>)dragController {
85   return dragController_.get();
87 @end
89 namespace {
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 {
102  public:
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]);
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 pinned 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     }
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));
195     }
196     if ([controller shouldShowIcon] && [controller shouldShowMediaIndicator]) {
197       EXPECT_LE(NSMaxX([[controller iconView] frame]),
198                 NSMinX([[controller mediaIndicatorButton] frame]));
199     }
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));
208     }
209     if ([controller shouldShowMediaIndicator] &&
210         [controller shouldShowCloseButton]) {
211       EXPECT_LE(NSMaxX([[controller mediaIndicatorButton] 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   }
224  private:
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
289 // get called.
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
315                                    target:target.get()
316                                  selector:@selector(mouseTimer:)
317                                  userInfo:window
318                                   repeats:NO];
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
323                                      location:click_point
324                                 modifierFlags:0
325                                     timestamp:[current timestamp]
326                                  windowNumber:[window windowNumber]
327                                       context:nil
328                                   eventNumber:0
329                                    clickCount:1
330                                      pressure:1.0];
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];
344   EXPECT_GE(cap, 1);
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];
358   EXPECT_GT(cap, 0);
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];
397   EXPECT_GT(cap, 0);
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];
409   EXPECT_GT(cap, 0);
411   // Asking the view for its menu should yield a valid menu.
412   NSMenu* menu = [[controller view] menu];
413   EXPECT_TRUE(menu);
414   EXPECT_EQ(3, [menu numberOfItems]);
417 // Tests that the title field is correctly positioned and sized when the
418 // view is resized.
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
483   };
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]];
491   // Create favicon.
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
504   // results.
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);
509            ++mediaStateIndex) {
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];
526         int minWidth;
527         if (isPinnedTab) {
528           tabFrame.size.width = minWidth = [TabController pinnedTabWidth];
529         } else {
530           tabFrame.size.width = [TabController maxTabWidth];
531           minWidth = isActiveTab ? [TabController minActiveTabWidth] :
532               [TabController minTabWidth];
533         }
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;
539         }
540       }
541     }
542   }
545 }  // namespace