// Copyright (c) 2012 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #import #include "base/bind_helpers.h" #include "base/mac/scoped_nsautorelease_pool.h" #include "chrome/browser/media/media_capture_devices_dispatcher.h" #include "chrome/browser/media/media_stream_capture_indicator.h" #include "chrome/browser/ui/browser_window.h" #import "chrome/browser/ui/cocoa/browser_window_controller.h" #include "chrome/browser/ui/cocoa/cocoa_profile_test.h" #import "chrome/browser/ui/cocoa/new_tab_button.h" #import "chrome/browser/ui/cocoa/tabs/tab_controller.h" #import "chrome/browser/ui/cocoa/tabs/tab_strip_controller.h" #import "chrome/browser/ui/cocoa/tabs/tab_strip_view.h" #import "chrome/browser/ui/cocoa/tabs/tab_view.h" #include "chrome/browser/ui/tabs/tab_utils.h" #include "chrome/browser/ui/tabs/test_tab_strip_model_delegate.h" #include "chrome/test/base/testing_profile.h" #include "content/public/browser/site_instance.h" #include "content/public/browser/web_contents.h" #include "content/public/common/media_stream_request.h" #include "testing/gtest/include/gtest/gtest.h" #import "testing/gtest_mac.h" #include "testing/platform_test.h" #include "ui/events/test/cocoa_test_event_utils.h" using content::SiteInstance; using content::WebContents; @interface TabStripControllerForMediaTesting : TabStripController { // Keeps media state of tabs in browser for testing purpose. std::map contentsMediaStateMaps_; } @end @implementation TabStripControllerForMediaTesting // Returns the media state of each tab from the map we are keeping. - (TabMediaState)mediaStateForContents:(content::WebContents*)contents { return contentsMediaStateMaps_[contents]; } - (void)setMediaStateForContents:(content::WebContents*)contents withMediaState:(TabMediaState)media_state { contentsMediaStateMaps_[contents] = media_state; } @end @interface TestTabStripControllerDelegate : NSObject { } @end @implementation TestTabStripControllerDelegate - (void)onActivateTabWithContents:(WebContents*)contents { } - (void)onTabChanged:(TabStripModelObserver::TabChangeType)change withContents:(WebContents*)contents { } - (void)onTabDetachedWithContents:(WebContents*)contents { } @end // Helper class for invoking a base::Closure via // -performSelector:withObject:afterDelay:. @interface TestClosureRunner : NSObject { @private base::Closure closure_; } - (id)initWithClosure:(const base::Closure&)closure; - (void)scheduleDelayedRun; - (void)run; @end @implementation TestClosureRunner - (id)initWithClosure:(const base::Closure&)closure { if (self) { closure_ = closure; } return self; } - (void)scheduleDelayedRun { [self performSelector:@selector(run) withObject:nil afterDelay:0]; } - (void)run { closure_.Run(); } @end @interface TabStripController (Test) - (void)mouseMoved:(NSEvent*)event; @end @implementation TabView (Test) - (TabController*)controller { return controller_; } @end namespace { class TabStripControllerTest : public CocoaProfileTest { public: void SetUp() override { CocoaProfileTest::SetUp(); ASSERT_TRUE(browser()); NSWindow* window = browser()->window()->GetNativeWindow(); NSView* parent = [window contentView]; NSRect content_frame = [parent frame]; // Create the "switch view" (view that gets changed out when a tab // switches). NSRect switch_frame = NSMakeRect(0, 0, content_frame.size.width, 500); base::scoped_nsobject switch_view( [[NSView alloc] initWithFrame:switch_frame]); switch_view_ = switch_view; [parent addSubview:switch_view_.get()]; // Create the tab strip view. It's expected to have a child button in it // already as the "new tab" button so create that too. NSRect strip_frame = NSMakeRect(0, NSMaxY(switch_frame), content_frame.size.width, 30); tab_strip_.reset( [[TabStripView alloc] initWithFrame:strip_frame]); [parent addSubview:tab_strip_.get()]; NSRect button_frame = NSMakeRect(0, 0, 15, 15); base::scoped_nsobject new_tab_button( [[NewTabButton alloc] initWithFrame:button_frame]); [tab_strip_ addSubview:new_tab_button.get()]; [tab_strip_ setNewTabButton:new_tab_button.get()]; delegate_.reset(new TestTabStripModelDelegate()); model_ = browser()->tab_strip_model(); controller_delegate_.reset([TestTabStripControllerDelegate alloc]); controller_.reset([[TabStripController alloc] initWithView:static_cast(tab_strip_.get()) switchView:switch_view.get() browser:browser() delegate:controller_delegate_.get()]); } void TearDown() override { // The call to CocoaTest::TearDown() deletes the Browser and TabStripModel // objects, so we first have to delete the controller, which refers to them. controller_.reset(); model_ = NULL; CocoaProfileTest::TearDown(); } // Return a derived TabStripController. TabStripControllerForMediaTesting* InitTabStripControllerForMediaTesting() { TabStripControllerForMediaTesting* c = [[TabStripControllerForMediaTesting alloc] initWithView:static_cast(tab_strip_.get()) switchView:switch_view_.get() browser:browser() delegate:controller_delegate_.get()]; return c; } TabView* CreateTab() { SiteInstance* instance = SiteInstance::Create(profile()); WebContents* web_contents = WebContents::Create( content::WebContents::CreateParams(profile(), instance)); model_->AppendWebContents(web_contents, true); const NSUInteger tab_count = [controller_.get() viewsCount]; return static_cast([controller_.get() viewAtIndex:tab_count - 1]); } // Closes all tabs and unrefs the tabstrip and then posts a NSLeftMouseUp // event which should end the nested drag event loop. void CloseTabsAndEndDrag() { // Simulate a close of the browser window. model_->CloseAllTabs(); controller_.reset(); tab_strip_.reset(); // Schedule a NSLeftMouseUp to end the nested drag event loop. NSEvent* event = cocoa_test_event_utils::MouseEventWithType(NSLeftMouseUp, 0); [NSApp postEvent:event atStart:NO]; } scoped_ptr delegate_; TabStripModel* model_; base::scoped_nsobject controller_delegate_; base::scoped_nsobject controller_; base::scoped_nsobject tab_strip_; base::scoped_nsobject switch_view_; }; // Test adding and removing tabs and making sure that views get added to // the tab strip. TEST_F(TabStripControllerTest, AddRemoveTabs) { EXPECT_TRUE(model_->empty()); CreateTab(); EXPECT_EQ(model_->count(), 1); } // Clicking a selected (but inactive) tab should activate it. TEST_F(TabStripControllerTest, ActivateSelectedButInactiveTab) { TabView* tab0 = CreateTab(); TabView* tab1 = CreateTab(); model_->ToggleSelectionAt(0); EXPECT_TRUE([[tab0 controller] selected]); EXPECT_TRUE([[tab1 controller] selected]); [controller_ selectTab:tab1]; EXPECT_EQ(1, model_->active_index()); } // Toggling (cmd-click) a selected (but inactive) tab should deselect it. TEST_F(TabStripControllerTest, DeselectInactiveTab) { TabView* tab0 = CreateTab(); TabView* tab1 = CreateTab(); model_->ToggleSelectionAt(0); EXPECT_TRUE([[tab0 controller] selected]); EXPECT_TRUE([[tab1 controller] selected]); model_->ToggleSelectionAt(1); EXPECT_TRUE([[tab0 controller] selected]); EXPECT_FALSE([[tab1 controller] selected]); } TEST_F(TabStripControllerTest, SelectTab) { // TODO(pinkerton): Implement http://crbug.com/10899 } TEST_F(TabStripControllerTest, RearrangeTabs) { // TODO(pinkerton): Implement http://crbug.com/10899 } TEST_F(TabStripControllerTest, CorrectMouseHoverBehavior) { TabView* tab1 = CreateTab(); TabView* tab2 = CreateTab(); EXPECT_FALSE([tab1 controller].selected); EXPECT_TRUE([tab2 controller].selected); // Check that there's no hovered tab yet. EXPECT_FALSE([controller_ hoveredTab]); // Set up mouse event on overlap of tab1 + tab2. const CGFloat min_y = NSMinY([tab_strip_.get() frame]) + 1; // Hover over overlap between tab 1 and 2. NSEvent* event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(280, min_y), NSMouseMoved, 0); [controller_.get() mouseMoved:event]; EXPECT_EQ(tab2, [controller_ hoveredTab]); // Hover over tab 1. event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(260, min_y), NSMouseMoved, 0); [controller_.get() mouseMoved:event]; EXPECT_EQ(tab1, [controller_ hoveredTab]); // Hover over tab 2. event = cocoa_test_event_utils::MouseEventAtPoint(NSMakePoint(290, min_y), NSMouseMoved, 0); [controller_.get() mouseMoved:event]; EXPECT_EQ(tab2, [controller_ hoveredTab]); } TEST_F(TabStripControllerTest, CorrectTitleAndToolTipTextFromSetTabTitle) { using content::MediaStreamDevice; using content::MediaStreamDevices; using content::MediaStreamUI; TabView* const tab = CreateTab(); TabController* const tabController = [tab controller]; WebContents* const contents = model_->GetActiveWebContents(); // Initially, tab title and tooltip text are equivalent. EXPECT_EQ(TAB_MEDIA_STATE_NONE, chrome::GetTabMediaStateForContents(contents)); [controller_ setTabTitle:tabController withContents:contents]; NSString* const baseTitle = [tabController title]; EXPECT_NSEQ(baseTitle, [tabController toolTip]); // Simulate the start of tab video capture. Tab title remains the same, but // the tooltip text should include the following appended: 1) a line break; // 2) a non-empty string with a localized description of the media state. scoped_refptr indicator = MediaCaptureDevicesDispatcher::GetInstance()-> GetMediaStreamCaptureIndicator(); const MediaStreamDevice dummyVideoCaptureDevice( content::MEDIA_TAB_VIDEO_CAPTURE, "dummy_id", "dummy name"); scoped_ptr streamUi(indicator->RegisterMediaStream( contents, MediaStreamDevices(1, dummyVideoCaptureDevice))); streamUi->OnStarted(base::Bind(&base::DoNothing)); EXPECT_EQ(TAB_MEDIA_STATE_CAPTURING, chrome::GetTabMediaStateForContents(contents)); [controller_ setTabTitle:tabController withContents:contents]; EXPECT_NSEQ(baseTitle, [tabController title]); NSString* const toolTipText = [tabController toolTip]; if ([baseTitle length] > 0) { EXPECT_TRUE(NSEqualRanges(NSMakeRange(0, [baseTitle length]), [toolTipText rangeOfString:baseTitle])); EXPECT_TRUE(NSEqualRanges(NSMakeRange([baseTitle length], 1), [toolTipText rangeOfString:@"\n"])); EXPECT_LT([baseTitle length] + 1, [toolTipText length]); } else { EXPECT_LT(0u, [toolTipText length]); } // Simulate the end of tab video capture. Tab title and tooltip should become // equivalent again. streamUi.reset(); EXPECT_EQ(TAB_MEDIA_STATE_NONE, chrome::GetTabMediaStateForContents(contents)); [controller_ setTabTitle:tabController withContents:contents]; EXPECT_NSEQ(baseTitle, [tabController title]); EXPECT_NSEQ(baseTitle, [tabController toolTip]); } TEST_F(TabStripControllerTest, TabCloseDuringDrag) { TabController* tab; // The TabController gets autoreleased when created, but is owned by the // tab strip model. Use a ScopedNSAutoreleasePool to get a truly weak ref // to it to test that -maybeStartDrag:forTab: can handle that properly. { base::mac::ScopedNSAutoreleasePool pool; tab = [CreateTab() controller]; } // Schedule a task to close all the tabs and stop the drag, before the call to // -maybeStartDrag:forTab:, which starts a nested event loop. This task will // run in that nested event loop, which shouldn't crash. base::scoped_nsobject runner([[TestClosureRunner alloc] initWithClosure:base::Bind(&TabStripControllerTest::CloseTabsAndEndDrag, base::Unretained(this))]); [runner scheduleDelayedRun]; NSEvent* event = cocoa_test_event_utils::LeftMouseDownAtPoint(NSZeroPoint); [[controller_ dragController] maybeStartDrag:event forTab:tab]; } TEST_F(TabStripControllerTest, ViewAccessibility_Contents) { NSArray* attrs = [tab_strip_ accessibilityAttributeNames]; ASSERT_TRUE([attrs containsObject:NSAccessibilityContentsAttribute]); // Create two tabs and ensure they exist in the contents array. TabView* tab1 = CreateTab(); TabView* tab2 = CreateTab(); NSObject* contents = [tab_strip_ accessibilityAttributeValue:NSAccessibilityContentsAttribute]; DCHECK([contents isKindOfClass:[NSArray class]]); NSArray* contentsArray = static_cast(contents); ASSERT_TRUE([contentsArray containsObject:tab1]); ASSERT_TRUE([contentsArray containsObject:tab2]); } TEST_F(TabStripControllerTest, ViewAccessibility_Value) { NSArray* attrs = [tab_strip_ accessibilityAttributeNames]; ASSERT_TRUE([attrs containsObject:NSAccessibilityValueAttribute]); // Create two tabs and ensure the active one gets returned. TabView* tab1 = CreateTab(); TabView* tab2 = CreateTab(); EXPECT_FALSE([tab1 controller].selected); EXPECT_TRUE([tab2 controller].selected); NSObject* value = [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute]; EXPECT_EQ(tab2, value); model_->ActivateTabAt(0, false); EXPECT_TRUE([tab1 controller].selected); EXPECT_FALSE([tab2 controller].selected); value = [tab_strip_ accessibilityAttributeValue:NSAccessibilityValueAttribute]; EXPECT_EQ(tab1, value); } TEST_F(TabStripControllerTest, CorrectWindowFromUpdateWindowMediaState) { controller_.reset(InitTabStripControllerForMediaTesting()); NSWindow* window = [tab_strip_ window]; BrowserWindowController* window_controller = [BrowserWindowController browserWindowControllerForWindow:window]; TabStripControllerForMediaTesting* tabStripControllerForTesting = static_cast(controller_); TabView* const tab1 = CreateTab(); TabView* const tab2 = CreateTab(); // tab2 should be the selected one. EXPECT_FALSE([tab1 controller].selected); EXPECT_TRUE([tab2 controller].selected); WebContents* const contents_at_tab1 = model_->GetActiveWebContents(); [tabStripControllerForTesting setMediaStateForContents:contents_at_tab1 withMediaState:TAB_MEDIA_STATE_AUDIO_PLAYING]; // Make sure the overriden from base controller correctly handles media // status of tabs. EXPECT_EQ(TAB_MEDIA_STATE_AUDIO_PLAYING, [controller_ mediaStateForContents:contents_at_tab1]); [controller_ updateWindowMediaState:TAB_MEDIA_STATE_AUDIO_PLAYING forWebContents:contents_at_tab1]; // Because we have one tab playing, and the other one's media state is none, // window media state should be AUDIO_PLAYING. EXPECT_EQ(TAB_MEDIA_STATE_AUDIO_PLAYING, [window_controller mediaState]); model_->ActivateTabAt(0, false); // tab1 should be the selected one now. EXPECT_TRUE([tab1 controller].selected); EXPECT_FALSE([tab2 controller].selected); WebContents* const contents_at_tab0 = model_->GetActiveWebContents(); [tabStripControllerForTesting setMediaStateForContents:contents_at_tab0 withMediaState:TAB_MEDIA_STATE_AUDIO_MUTING]; [controller_ updateWindowMediaState:TAB_MEDIA_STATE_AUDIO_MUTING forWebContents:contents_at_tab0]; // We have two tabs. One is playing and the other one is muting. The window // media state should be still AUDIO_PLAYING. EXPECT_EQ(TAB_MEDIA_STATE_AUDIO_PLAYING, [window_controller mediaState]); [tabStripControllerForTesting setMediaStateForContents:contents_at_tab1 withMediaState:TAB_MEDIA_STATE_AUDIO_MUTING]; [controller_ updateWindowMediaState:TAB_MEDIA_STATE_AUDIO_MUTING forWebContents:contents_at_tab1]; // Now both tabs are muting, the window media state should be AUDIO_MUTING. EXPECT_EQ(TAB_MEDIA_STATE_AUDIO_MUTING, [window_controller mediaState]); [tabStripControllerForTesting setMediaStateForContents:contents_at_tab0 withMediaState:TAB_MEDIA_STATE_AUDIO_PLAYING]; [controller_ updateWindowMediaState:TAB_MEDIA_STATE_AUDIO_PLAYING forWebContents:contents_at_tab0]; // Among those tabs which were muting, one is started playing, the window // media state should be playing. EXPECT_EQ(TAB_MEDIA_STATE_AUDIO_PLAYING, [window_controller mediaState]); // Mute it again for further testing. [tabStripControllerForTesting setMediaStateForContents:contents_at_tab0 withMediaState:TAB_MEDIA_STATE_AUDIO_MUTING]; [controller_ updateWindowMediaState:TAB_MEDIA_STATE_AUDIO_MUTING forWebContents:contents_at_tab0]; [tabStripControllerForTesting setMediaStateForContents:contents_at_tab1 withMediaState:TAB_MEDIA_STATE_NONE]; [controller_ updateWindowMediaState:TAB_MEDIA_STATE_NONE forWebContents:contents_at_tab1]; // One of the tabs is muting, the other one is none. So window media state // should be MUTING. EXPECT_EQ(TAB_MEDIA_STATE_AUDIO_MUTING, [window_controller mediaState]); [tabStripControllerForTesting setMediaStateForContents:contents_at_tab0 withMediaState:TAB_MEDIA_STATE_NONE]; [controller_ updateWindowMediaState:TAB_MEDIA_STATE_NONE forWebContents:contents_at_tab0]; // Neither of tabs playing nor muting, so the window media state should be // NONE. EXPECT_EQ(TAB_MEDIA_STATE_NONE, [window_controller mediaState]); } } // namespace