1 // Copyright (c) 2012 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 #include "base/bind_helpers.h"
8 #include "base/mac/scoped_nsautorelease_pool.h"
9 #include "chrome/browser/media/media_capture_devices_dispatcher.h"
10 #include "chrome/browser/media/media_stream_capture_indicator.h"
11 #include "chrome/browser/ui/browser_window.h"
12 #include "chrome/browser/ui/cocoa/cocoa_profile_test.h"
13 #import "chrome/browser/ui/cocoa/new_tab_button.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
15 #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h"
16 #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h"
17 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
18 #include "chrome/browser/ui/tabs/tab_utils.h"
19 #include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h"
20 #include "chrome/test/base/testing_profile.h"
21 #include "content/public/browser/site_instance.h"
22 #include "content/public/browser/web_contents.h"
23 #include "content/public/common/media_stream_request.h"
24 #include "testing/gtest/include/gtest/gtest.h"
25 #import "testing/gtest_mac.h"
26 #include "testing/platform_test.h"
27 #include "ui/events/test/cocoa_test_event_utils.h"
29 using content::SiteInstance;
30 using content::WebContents;
32 @interface TestTabStripControllerDelegate
33 : NSObject<TabStripControllerDelegate> {
37 @implementation TestTabStripControllerDelegate
38 - (void)onActivateTabWithContents:(WebContents*)contents {
40 - (void)onTabChanged:(TabStripModelObserver::TabChangeType)change
41 withContents:(WebContents*)contents {
43 - (void)onTabDetachedWithContents:(WebContents*)contents {
48 // Helper class for invoking a base::Closure via
49 // -performSelector:withObject:afterDelay:.
50 @interface TestClosureRunner : NSObject {
52 base::Closure closure_;
54 - (id)initWithClosure:(const base::Closure&)closure;
55 - (void)scheduleDelayedRun;
59 @implementation TestClosureRunner
60 - (id)initWithClosure:(const base::Closure&)closure {
66 - (void)scheduleDelayedRun {
67 [self performSelector:@selector(run) withObject:nil afterDelay:0];
74 @interface TabStripController (Test)
76 - (void)mouseMoved:(NSEvent*)event;
80 @implementation TabView (Test)
82 - (TabController*)controller {
90 class TabStripControllerTest : public CocoaProfileTest {
92 virtual void SetUp() OVERRIDE {
93 CocoaProfileTest::SetUp();
94 ASSERT_TRUE(browser());
96 NSWindow* window = browser()->window()->GetNativeWindow();
97 NSView* parent = [window contentView];
98 NSRect content_frame = [parent frame];
100 // Create the "switch view" (view that gets changed out when a tab
102 NSRect switch_frame = NSMakeRect(0, 0, content_frame.size.width, 500);
103 base::scoped_nsobject<NSView> switch_view(
104 [[NSView alloc] initWithFrame:switch_frame]);
105 [parent addSubview:switch_view.get()];
107 // Create the tab strip view. It's expected to have a child button in it
108 // already as the "new tab" button so create that too.
109 NSRect strip_frame = NSMakeRect(0, NSMaxY(switch_frame),
110 content_frame.size.width, 30);
112 [[TabStripView alloc] initWithFrame:strip_frame]);
113 [parent addSubview:tab_strip_.get()];
114 NSRect button_frame = NSMakeRect(0, 0, 15, 15);
115 base::scoped_nsobject<NewTabButton> new_tab_button(
116 [[NewTabButton alloc] initWithFrame:button_frame]);
117 [tab_strip_ addSubview:new_tab_button.get()];
118 [tab_strip_ setNewTabButton:new_tab_button.get()];
120 delegate_.reset(new TestTabStripModelDelegate());
121 model_ = browser()->tab_strip_model();
122 controller_delegate_.reset([TestTabStripControllerDelegate alloc]);
123 controller_.reset([[TabStripController alloc]
124 initWithView:static_cast<TabStripView*>(tab_strip_.get())
125 switchView:switch_view.get()
127 delegate:controller_delegate_.get()]);
130 virtual void TearDown() OVERRIDE {
131 // The call to CocoaTest::TearDown() deletes the Browser and TabStripModel
132 // objects, so we first have to delete the controller, which refers to them.
135 CocoaProfileTest::TearDown();
138 TabView* CreateTab() {
139 SiteInstance* instance = SiteInstance::Create(profile());
140 WebContents* web_contents = WebContents::Create(
141 content::WebContents::CreateParams(profile(), instance));
142 model_->AppendWebContents(web_contents, true);
143 const NSUInteger tab_count = [controller_.get() viewsCount];
144 return static_cast<TabView*>([controller_.get() viewAtIndex:tab_count - 1]);
147 // Closes all tabs and unrefs the tabstrip and then posts a NSLeftMouseUp
148 // event which should end the nested drag event loop.
149 void CloseTabsAndEndDrag() {
150 // Simulate a close of the browser window.
151 model_->CloseAllTabs();
154 // Schedule a NSLeftMouseUp to end the nested drag event loop.
156 cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 0);
157 [NSApp postEvent:event atStart:NO];
160 scoped_ptr<TestTabStripModelDelegate> delegate_;
161 TabStripModel* model_;
162 base::scoped_nsobject<TestTabStripControllerDelegate> controller_delegate_;
163 base::scoped_nsobject<TabStripController> controller_;
164 base::scoped_nsobject<TabStripView> tab_strip_;
167 // Test adding and removing tabs and making sure that views get added to
169 TEST_F(TabStripControllerTest, AddRemoveTabs) {
170 EXPECT_TRUE(model_->empty());
172 EXPECT_EQ(model_->count(), 1);
175 // Clicking a selected (but inactive) tab should activate it.
176 TEST_F(TabStripControllerTest, ActivateSelectedButInactiveTab) {
177 TabView* tab0 = CreateTab();
178 TabView* tab1 = CreateTab();
179 model_->ToggleSelectionAt(0);
180 EXPECT_TRUE([[tab0 controller] selected]);
181 EXPECT_TRUE([[tab1 controller] selected]);
183 [controller_ selectTab:tab1];
184 EXPECT_EQ(1, model_->active_index());
187 // Toggling (cmd-click) a selected (but inactive) tab should deselect it.
188 TEST_F(TabStripControllerTest, DeselectInactiveTab) {
189 TabView* tab0 = CreateTab();
190 TabView* tab1 = CreateTab();
191 model_->ToggleSelectionAt(0);
192 EXPECT_TRUE([[tab0 controller] selected]);
193 EXPECT_TRUE([[tab1 controller] selected]);
195 model_->ToggleSelectionAt(1);
196 EXPECT_TRUE([[tab0 controller] selected]);
197 EXPECT_FALSE([[tab1 controller] selected]);
200 TEST_F(TabStripControllerTest, SelectTab) {
201 // TODO(pinkerton): Implement http://crbug.com/10899
204 TEST_F(TabStripControllerTest, RearrangeTabs) {
205 // TODO(pinkerton): Implement http://crbug.com/10899
208 TEST_F(TabStripControllerTest, CorrectToolTipMouseHoverBehavior) {
209 // Set tab 1 tooltip.
210 TabView* tab1 = CreateTab();
211 [tab1 setToolTip:@"Tab1"];
213 // Set tab 2 tooltip.
214 TabView* tab2 = CreateTab();
215 [tab2 setToolTip:@"Tab2"];
217 EXPECT_FALSE([tab1 controller].selected);
218 EXPECT_TRUE([tab2 controller].selected);
220 // Check that there's no tooltip yet.
221 EXPECT_FALSE([controller_ view:nil
226 // Set up mouse event on overlap of tab1 + tab2.
227 const CGFloat min_y = NSMinY([tab_strip_.get() frame]) + 1;
229 // Hover over overlap between tab 1 and 2.
231 cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(280, min_y),
233 [controller_.get() mouseMoved:event];
235 [[controller_ view:nil
238 userData:nil] cStringUsingEncoding:NSASCIIStringEncoding]);
242 event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(260, min_y),
244 [controller_.get() mouseMoved:event];
246 [[controller_ view:nil
249 userData:nil] cStringUsingEncoding:NSASCIIStringEncoding]);
252 event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(290, min_y),
254 [controller_.get() mouseMoved:event];
256 [[controller_ view:nil
259 userData:nil] cStringUsingEncoding:NSASCIIStringEncoding]);
262 TEST_F(TabStripControllerTest, CorrectTitleAndToolTipTextFromSetTabTitle) {
263 using content::MediaStreamDevice;
264 using content::MediaStreamDevices;
265 using content::MediaStreamUI;
267 TabView* const tab = CreateTab();
268 TabController* const tabController = [tab controller];
269 WebContents* const contents = model_->GetActiveWebContents();
271 // Initially, tab title and tooltip text are equivalent.
272 EXPECT_EQ(TAB_MEDIA_STATE_NONE,
273 chrome::GetTabMediaStateForContents(contents));
274 [controller_ setTabTitle:tabController withContents:contents];
275 NSString* const baseTitle = [tabController title];
276 EXPECT_NSEQ(baseTitle, [tabController toolTip]);
278 // Simulate the start of tab video capture. Tab title remains the same, but
279 // the tooltip text should include the following appended: 1) a line break;
280 // 2) a non-empty string with a localized description of the media state.
281 scoped_refptr<MediaStreamCaptureIndicator> indicator =
282 MediaCaptureDevicesDispatcher::GetInstance()->
283 GetMediaStreamCaptureIndicator();
284 const MediaStreamDevice dummyVideoCaptureDevice(
285 content::MEDIA_TAB_VIDEO_CAPTURE, "dummy_id", "dummy name");
286 scoped_ptr<MediaStreamUI> streamUi(indicator->RegisterMediaStream(
287 contents, MediaStreamDevices(1, dummyVideoCaptureDevice)));
288 streamUi->OnStarted(base::Bind(&base::DoNothing));
289 EXPECT_EQ(TAB_MEDIA_STATE_CAPTURING,
290 chrome::GetTabMediaStateForContents(contents));
291 [controller_ setTabTitle:tabController withContents:contents];
292 EXPECT_NSEQ(baseTitle, [tabController title]);
293 NSString* const toolTipText = [tabController toolTip];
294 if ([baseTitle length] > 0) {
295 EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, [baseTitle length]),
296 [toolTipText rangeOfString:baseTitle]));
297 EXPECT_TRUE(NSEqualRanges(NSMakeRange([baseTitle length], 1),
298 [toolTipText rangeOfString:@"\n"]));
299 EXPECT_LT([baseTitle length] + 1, [toolTipText length]);
301 EXPECT_LT(0u, [toolTipText length]);
304 // Simulate the end of tab video capture. Tab title and tooltip should become
307 EXPECT_EQ(TAB_MEDIA_STATE_NONE,
308 chrome::GetTabMediaStateForContents(contents));
309 [controller_ setTabTitle:tabController withContents:contents];
310 EXPECT_NSEQ(baseTitle, [tabController title]);
311 EXPECT_NSEQ(baseTitle, [tabController toolTip]);
314 TEST_F(TabStripControllerTest, TabCloseDuringDrag) {
316 // The TabController gets autoreleased when created, but is owned by the
317 // tab strip model. Use a ScopedNSAutoreleasePool to get a truly weak ref
318 // to it to test that -maybeStartDrag:forTab: can handle that properly.
320 base::mac::ScopedNSAutoreleasePool pool;
321 tab = [CreateTab() controller];
324 // Schedule a task to close all the tabs and stop the drag, before the call to
325 // -maybeStartDrag:forTab:, which starts a nested event loop. This task will
326 // run in that nested event loop, which shouldn't crash.
327 base::scoped_nsobject<TestClosureRunner> runner([[TestClosureRunner alloc]
328 initWithClosure:base::Bind(&TabStripControllerTest::CloseTabsAndEndDrag,
329 base::Unretained(this))]);
330 [runner scheduleDelayedRun];
333 cocoa_test_event_utils::LeftMouseDownAtPoint(NSZeroPoint);
334 [[controller_ dragController] maybeStartDrag:event forTab:tab];
337 TEST_F(TabStripControllerTest, ViewAccessibility_Contents) {
338 NSArray* attrs = [tab_strip_ accessibilityAttributeNames];
339 ASSERT_TRUE([attrs containsObject:NSAccessibilityContentsAttribute]);
341 // Create two tabs and ensure they exist in the contents array.
342 TabView* tab1 = CreateTab();
343 TabView* tab2 = CreateTab();
345 [tab_strip_ accessibilityAttributeValue:NSAccessibilityContentsAttribute];
346 DCHECK([contents isKindOfClass:[NSArray class]]);
347 NSArray* contentsArray = static_cast<NSArray*>(contents);
348 ASSERT_TRUE([contentsArray containsObject:tab1]);
349 ASSERT_TRUE([contentsArray containsObject:tab2]);
352 TEST_F(TabStripControllerTest, ViewAccessibility_Value) {
353 NSArray* attrs = [tab_strip_ accessibilityAttributeNames];
354 ASSERT_TRUE([attrs containsObject:NSAccessibilityValueAttribute]);
356 // Create two tabs and ensure the active one gets returned.
357 TabView* tab1 = CreateTab();
358 TabView* tab2 = CreateTab();
359 EXPECT_FALSE([tab1 controller].selected);
360 EXPECT_TRUE([tab2 controller].selected);
362 [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute];
363 EXPECT_EQ(tab2, value);
365 model_->ActivateTabAt(0, false);
366 EXPECT_TRUE([tab1 controller].selected);
367 EXPECT_FALSE([tab2 controller].selected);
369 [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute];
370 EXPECT_EQ(tab1, value);