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