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 #import "testing/gtest_mac.h"
25 #include "testing/gtest/include/gtest/gtest.h"
26 #include "testing/platform_test.h"
27 #include "ui/base/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 TEST_F(TabStripControllerTest, SelectTab) {
176 // TODO(pinkerton): Implement http://crbug.com/10899
179 TEST_F(TabStripControllerTest, RearrangeTabs) {
180 // TODO(pinkerton): Implement http://crbug.com/10899
183 TEST_F(TabStripControllerTest, CorrectToolTipMouseHoverBehavior) {
184 // Set tab 1 tooltip.
185 TabView* tab1 = CreateTab();
186 [tab1 setToolTip:@"Tab1"];
188 // Set tab 2 tooltip.
189 TabView* tab2 = CreateTab();
190 [tab2 setToolTip:@"Tab2"];
192 EXPECT_FALSE([tab1 controller].selected);
193 EXPECT_TRUE([tab2 controller].selected);
195 // Check that there's no tooltip yet.
196 EXPECT_FALSE([controller_ view:nil
201 // Set up mouse event on overlap of tab1 + tab2.
202 const CGFloat min_y = NSMinY([tab_strip_.get() frame]) + 1;
204 // Hover over overlap between tab 1 and 2.
206 cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(280, min_y),
208 [controller_.get() mouseMoved:event];
210 [[controller_ view:nil
213 userData:nil] cStringUsingEncoding:NSASCIIStringEncoding]);
217 event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(260, min_y),
219 [controller_.get() mouseMoved:event];
221 [[controller_ view:nil
224 userData:nil] cStringUsingEncoding:NSASCIIStringEncoding]);
227 event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(290, min_y),
229 [controller_.get() mouseMoved:event];
231 [[controller_ view:nil
234 userData:nil] cStringUsingEncoding:NSASCIIStringEncoding]);
237 TEST_F(TabStripControllerTest, CorrectTitleAndToolTipTextFromSetTabTitle) {
238 using content::MediaStreamDevice;
239 using content::MediaStreamDevices;
240 using content::MediaStreamUI;
242 TabView* const tab = CreateTab();
243 TabController* const tabController = [tab controller];
244 WebContents* const contents = model_->GetActiveWebContents();
246 // Initially, tab title and tooltip text are equivalent.
247 EXPECT_EQ(TAB_MEDIA_STATE_NONE,
248 chrome::GetTabMediaStateForContents(contents));
249 [controller_ setTabTitle:tabController withContents:contents];
250 NSString* const baseTitle = [tabController title];
251 EXPECT_NSEQ(baseTitle, [tabController toolTip]);
253 // Simulate the start of tab video capture. Tab title remains the same, but
254 // the tooltip text should include the following appended: 1) a line break;
255 // 2) a non-empty string with a localized description of the media state.
256 scoped_refptr<MediaStreamCaptureIndicator> indicator =
257 MediaCaptureDevicesDispatcher::GetInstance()->
258 GetMediaStreamCaptureIndicator();
259 const MediaStreamDevice dummyVideoCaptureDevice(
260 content::MEDIA_TAB_VIDEO_CAPTURE, "dummy_id", "dummy name");
261 scoped_ptr<MediaStreamUI> streamUi(indicator->RegisterMediaStream(
262 contents, MediaStreamDevices(1, dummyVideoCaptureDevice)));
263 streamUi->OnStarted(base::Bind(&base::DoNothing));
264 EXPECT_EQ(TAB_MEDIA_STATE_CAPTURING,
265 chrome::GetTabMediaStateForContents(contents));
266 [controller_ setTabTitle:tabController withContents:contents];
267 EXPECT_NSEQ(baseTitle, [tabController title]);
268 NSString* const toolTipText = [tabController toolTip];
269 if ([baseTitle length] > 0) {
270 EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, [baseTitle length]),
271 [toolTipText rangeOfString:baseTitle]));
272 EXPECT_TRUE(NSEqualRanges(NSMakeRange([baseTitle length], 1),
273 [toolTipText rangeOfString:@"\n"]));
274 EXPECT_LT([baseTitle length] + 1, [toolTipText length]);
276 EXPECT_LT(0u, [toolTipText length]);
279 // Simulate the end of tab video capture. Tab title and tooltip should become
282 EXPECT_EQ(TAB_MEDIA_STATE_NONE,
283 chrome::GetTabMediaStateForContents(contents));
284 [controller_ setTabTitle:tabController withContents:contents];
285 EXPECT_NSEQ(baseTitle, [tabController title]);
286 EXPECT_NSEQ(baseTitle, [tabController toolTip]);
289 TEST_F(TabStripControllerTest, TabCloseDuringDrag) {
291 // The TabController gets autoreleased when created, but is owned by the
292 // tab strip model. Use a ScopedNSAutoreleasePool to get a truly weak ref
293 // to it to test that -maybeStartDrag:forTab: can handle that properly.
295 base::mac::ScopedNSAutoreleasePool pool;
296 tab = [CreateTab() controller];
299 // Schedule a task to close all the tabs and stop the drag, before the call to
300 // -maybeStartDrag:forTab:, which starts a nested event loop. This task will
301 // run in that nested event loop, which shouldn't crash.
302 base::scoped_nsobject<TestClosureRunner> runner([[TestClosureRunner alloc]
303 initWithClosure:base::Bind(&TabStripControllerTest::CloseTabsAndEndDrag,
304 base::Unretained(this))]);
305 [runner scheduleDelayedRun];
308 cocoa_test_event_utils::LeftMouseDownAtPoint(NSZeroPoint);
309 [[controller_ dragController] maybeStartDrag:event forTab:tab];
312 TEST_F(TabStripControllerTest, ViewAccessibility_Contents) {
313 NSArray* attrs = [tab_strip_ accessibilityAttributeNames];
314 ASSERT_TRUE([attrs containsObject:NSAccessibilityContentsAttribute]);
316 // Create two tabs and ensure they exist in the contents array.
317 TabView* tab1 = CreateTab();
318 TabView* tab2 = CreateTab();
320 [tab_strip_ accessibilityAttributeValue:NSAccessibilityContentsAttribute];
321 DCHECK([contents isKindOfClass:[NSArray class]]);
322 NSArray* contentsArray = static_cast<NSArray*>(contents);
323 ASSERT_TRUE([contentsArray containsObject:tab1]);
324 ASSERT_TRUE([contentsArray containsObject:tab2]);
327 TEST_F(TabStripControllerTest, ViewAccessibility_Value) {
328 NSArray* attrs = [tab_strip_ accessibilityAttributeNames];
329 ASSERT_TRUE([attrs containsObject:NSAccessibilityValueAttribute]);
331 // Create two tabs and ensure the active one gets returned.
332 TabView* tab1 = CreateTab();
333 TabView* tab2 = CreateTab();
334 EXPECT_FALSE([tab1 controller].selected);
335 EXPECT_TRUE([tab2 controller].selected);
337 [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute];
338 EXPECT_EQ(tab2, value);
340 model_->ActivateTabAt(0, false);
341 EXPECT_TRUE([tab1 controller].selected);
342 EXPECT_FALSE([tab2 controller].selected);
344 [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute];
345 EXPECT_EQ(tab1, value);