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 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 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, CorrectMouseHoverBehavior) {
209 TabView* tab1 = CreateTab();
210 TabView* tab2 = CreateTab();
212 EXPECT_FALSE([tab1 controller].selected);
213 EXPECT_TRUE([tab2 controller].selected);
215 // Check that there's no hovered tab yet.
216 EXPECT_FALSE([controller_ hoveredTab]);
218 // Set up mouse event on overlap of tab1 + tab2.
219 const CGFloat min_y = NSMinY([tab_strip_.get() frame]) + 1;
221 // Hover over overlap between tab 1 and 2.
223 cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(280, min_y),
225 [controller_.get() mouseMoved:event];
226 EXPECT_EQ(tab2, [controller_ hoveredTab]);
229 event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(260, min_y),
231 [controller_.get() mouseMoved:event];
232 EXPECT_EQ(tab1, [controller_ hoveredTab]);
235 event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(290, min_y),
237 [controller_.get() mouseMoved:event];
238 EXPECT_EQ(tab2, [controller_ hoveredTab]);
241 TEST_F(TabStripControllerTest, CorrectTitleAndToolTipTextFromSetTabTitle) {
242 using content::MediaStreamDevice;
243 using content::MediaStreamDevices;
244 using content::MediaStreamUI;
246 TabView* const tab = CreateTab();
247 TabController* const tabController = [tab controller];
248 WebContents* const contents = model_->GetActiveWebContents();
250 // Initially, tab title and tooltip text are equivalent.
251 EXPECT_EQ(TAB_MEDIA_STATE_NONE,
252 chrome::GetTabMediaStateForContents(contents));
253 [controller_ setTabTitle:tabController withContents:contents];
254 NSString* const baseTitle = [tabController title];
255 EXPECT_NSEQ(baseTitle, [tabController toolTip]);
257 // Simulate the start of tab video capture. Tab title remains the same, but
258 // the tooltip text should include the following appended: 1) a line break;
259 // 2) a non-empty string with a localized description of the media state.
260 scoped_refptr<MediaStreamCaptureIndicator> indicator =
261 MediaCaptureDevicesDispatcher::GetInstance()->
262 GetMediaStreamCaptureIndicator();
263 const MediaStreamDevice dummyVideoCaptureDevice(
264 content::MEDIA_TAB_VIDEO_CAPTURE, "dummy_id", "dummy name");
265 scoped_ptr<MediaStreamUI> streamUi(indicator->RegisterMediaStream(
266 contents, MediaStreamDevices(1, dummyVideoCaptureDevice)));
267 streamUi->OnStarted(base::Bind(&base::DoNothing));
268 EXPECT_EQ(TAB_MEDIA_STATE_CAPTURING,
269 chrome::GetTabMediaStateForContents(contents));
270 [controller_ setTabTitle:tabController withContents:contents];
271 EXPECT_NSEQ(baseTitle, [tabController title]);
272 NSString* const toolTipText = [tabController toolTip];
273 if ([baseTitle length] > 0) {
274 EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, [baseTitle length]),
275 [toolTipText rangeOfString:baseTitle]));
276 EXPECT_TRUE(NSEqualRanges(NSMakeRange([baseTitle length], 1),
277 [toolTipText rangeOfString:@"\n"]));
278 EXPECT_LT([baseTitle length] + 1, [toolTipText length]);
280 EXPECT_LT(0u, [toolTipText length]);
283 // Simulate the end of tab video capture. Tab title and tooltip should become
286 EXPECT_EQ(TAB_MEDIA_STATE_NONE,
287 chrome::GetTabMediaStateForContents(contents));
288 [controller_ setTabTitle:tabController withContents:contents];
289 EXPECT_NSEQ(baseTitle, [tabController title]);
290 EXPECT_NSEQ(baseTitle, [tabController toolTip]);
293 TEST_F(TabStripControllerTest, TabCloseDuringDrag) {
295 // The TabController gets autoreleased when created, but is owned by the
296 // tab strip model. Use a ScopedNSAutoreleasePool to get a truly weak ref
297 // to it to test that -maybeStartDrag:forTab: can handle that properly.
299 base::mac::ScopedNSAutoreleasePool pool;
300 tab = [CreateTab() controller];
303 // Schedule a task to close all the tabs and stop the drag, before the call to
304 // -maybeStartDrag:forTab:, which starts a nested event loop. This task will
305 // run in that nested event loop, which shouldn't crash.
306 base::scoped_nsobject<TestClosureRunner> runner([[TestClosureRunner alloc]
307 initWithClosure:base::Bind(&TabStripControllerTest::CloseTabsAndEndDrag,
308 base::Unretained(this))]);
309 [runner scheduleDelayedRun];
312 cocoa_test_event_utils::LeftMouseDownAtPoint(NSZeroPoint);
313 [[controller_ dragController] maybeStartDrag:event forTab:tab];
316 TEST_F(TabStripControllerTest, ViewAccessibility_Contents) {
317 NSArray* attrs = [tab_strip_ accessibilityAttributeNames];
318 ASSERT_TRUE([attrs containsObject:NSAccessibilityContentsAttribute]);
320 // Create two tabs and ensure they exist in the contents array.
321 TabView* tab1 = CreateTab();
322 TabView* tab2 = CreateTab();
324 [tab_strip_ accessibilityAttributeValue:NSAccessibilityContentsAttribute];
325 DCHECK([contents isKindOfClass:[NSArray class]]);
326 NSArray* contentsArray = static_cast<NSArray*>(contents);
327 ASSERT_TRUE([contentsArray containsObject:tab1]);
328 ASSERT_TRUE([contentsArray containsObject:tab2]);
331 TEST_F(TabStripControllerTest, ViewAccessibility_Value) {
332 NSArray* attrs = [tab_strip_ accessibilityAttributeNames];
333 ASSERT_TRUE([attrs containsObject:NSAccessibilityValueAttribute]);
335 // Create two tabs and ensure the active one gets returned.
336 TabView* tab1 = CreateTab();
337 TabView* tab2 = CreateTab();
338 EXPECT_FALSE([tab1 controller].selected);
339 EXPECT_TRUE([tab2 controller].selected);
341 [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute];
342 EXPECT_EQ(tab2, value);
344 model_->ActivateTabAt(0, false);
345 EXPECT_TRUE([tab1 controller].selected);
346 EXPECT_FALSE([tab2 controller].selected);
348 [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute];
349 EXPECT_EQ(tab1, value);