// 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 #import "base/mac/scoped_nsobject.h" #include "base/macros.h" #include "base/run_loop.h" #include "chrome/app/chrome_command_ids.h" #include "chrome/browser/command_updater.h" #include "chrome/browser/ui/browser.h" #include "chrome/browser/ui/browser_command_controller.h" #include "chrome/browser/ui/browser_commands.h" #include "chrome/browser/ui/browser_list.h" #include "chrome/browser/ui/browser_list_observer.h" #include "chrome/browser/ui/cocoa/cocoa_profile_test.h" #import "chrome/browser/ui/cocoa/image_button_cell.h" #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h" #import "chrome/browser/ui/cocoa/view_resizer_pong.h" #include "chrome/common/pref_names.h" #include "chrome/test/base/testing_profile.h" #include "components/prefs/pref_service.h" #include "testing/gtest/include/gtest/gtest.h" #import "testing/gtest_mac.h" #include "testing/platform_test.h" // An NSView that fakes out hitTest:. @interface HitView : NSView { id hitTestReturn_; } @end @implementation HitView - (void)setHitTestReturn:(id)rtn { hitTestReturn_ = rtn; } - (NSView *)hitTest:(NSPoint)aPoint { return hitTestReturn_; } @end // Records the last command id and enabled state it has received so it can be // queried by the tests to see if we got a notification or not. @interface TestToolbarController : ToolbarController { @private int lastCommand_; // Id of last received state change. bool lastState_; // State of last received state change. } @property(nonatomic, readonly) int lastCommand; @property(nonatomic, readonly) bool lastState; @end @implementation TestToolbarController @synthesize lastCommand = lastCommand_; @synthesize lastState = lastState_; - (void)enabledStateChangedForCommand:(int)command enabled:(bool)enabled { [super enabledStateChangedForCommand:command enabled:enabled]; lastCommand_ = command; lastState_ = enabled; } @end namespace { class ToolbarControllerTest : public CocoaProfileTest { public: // Indexes that match the ordering returned by the private ToolbarController // |-toolbarViews| method. enum SubviewIndex { kBackIndex, kForwardIndex, kReloadIndex, kHomeIndex, kAppMenuIndex, kLocationIndex, kBrowserActionContainerViewIndex }; void SetUp() override { CocoaProfileTest::SetUp(); ASSERT_TRUE(browser()); resizeDelegate_.reset([[ViewResizerPong alloc] init]); CommandUpdater* updater = browser()->command_controller()->command_updater(); // The default state for the commands is true, set a couple to false to // ensure they get picked up correct on initialization updater->UpdateCommandEnabled(IDC_BACK, false); updater->UpdateCommandEnabled(IDC_FORWARD, false); bar_.reset([[TestToolbarController alloc] initWithCommands:browser()->command_controller()->command_updater() profile:profile() browser:browser() resizeDelegate:resizeDelegate_.get()]); EXPECT_TRUE([bar_ view]); NSView* parent = [test_window() contentView]; [parent addSubview:[bar_ view]]; } void TearDown() override { // Releasing ToolbarController doesn't actually free it at this point, since // the NSViewController retains a reference to it from the nib loading. // As browser() is released in the superclass TearDown, call // -[ToolbarController browserWillBeDestroyed] to prevent a use after free // issue on the |browser_| pointer in LocationBarViewMac when // ToolbarController is actually freed (some time after this method is run). [bar_ browserWillBeDestroyed]; bar_.reset(); CocoaProfileTest::TearDown(); } // Make sure the enabled state of the view is the same as the corresponding // command in the updater. The views are in the declaration order of outlets. void CompareState(CommandUpdater* updater, NSArray* views) { EXPECT_EQ(updater->IsCommandEnabled(IDC_BACK), [[views objectAtIndex:kBackIndex] isEnabled] ? true : false); EXPECT_EQ(updater->IsCommandEnabled(IDC_FORWARD), [[views objectAtIndex:kForwardIndex] isEnabled] ? true : false); EXPECT_EQ(updater->IsCommandEnabled(IDC_RELOAD), [[views objectAtIndex:kReloadIndex] isEnabled] ? true : false); EXPECT_EQ(updater->IsCommandEnabled(IDC_HOME), [[views objectAtIndex:kHomeIndex] isEnabled] ? true : false); } NSView* GetSubviewAt(SubviewIndex index) { return [[bar_ toolbarViews] objectAtIndex:index]; } base::scoped_nsobject resizeDelegate_; base::scoped_nsobject bar_; }; TEST_VIEW(ToolbarControllerTest, [bar_ view]) // Test the initial state that everything is sync'd up TEST_F(ToolbarControllerTest, InitialState) { CommandUpdater* updater = browser()->command_controller()->command_updater(); CompareState(updater, [bar_ toolbarViews]); } // Make sure a "titlebar only" toolbar with location bar works. TEST_F(ToolbarControllerTest, TitlebarOnly) { NSView* view = [bar_ view]; [bar_ setHasToolbar:NO hasLocationBar:YES]; EXPECT_EQ(view, [bar_ view]); // Simulate a popup going fullscreen and back by performing the reparenting // that happens during fullscreen transitions NSView* superview = [view superview]; [view removeFromSuperview]; [superview addSubview:view]; EXPECT_EQ(view, [bar_ view]); } // Test updateVisibility with location bar only; this method is used by bookmark // apps, and should never be called when the toolbar is enabled. Ensure that the // buttons remain in the correct state. TEST_F(ToolbarControllerTest, UpdateVisibility) { NSView* view = [bar_ view]; // Test the escapable states first. [bar_ setHasToolbar:YES hasLocationBar:YES]; EXPECT_GT([[bar_ view] frame].size.height, 0); EXPECT_GT([[bar_ view] frame].size.height, [GetSubviewAt(kLocationIndex) frame].size.height); EXPECT_GT([[bar_ view] frame].size.width, [GetSubviewAt(kLocationIndex) frame].size.width); EXPECT_FALSE([view isHidden]); EXPECT_FALSE([GetSubviewAt(kLocationIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kBackIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kForwardIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kReloadIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kAppMenuIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kHomeIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBrowserActionContainerViewIndex) isHidden]); // For NO/NO, only the top level toolbar view is hidden. [bar_ setHasToolbar:NO hasLocationBar:NO]; EXPECT_TRUE([view isHidden]); EXPECT_FALSE([GetSubviewAt(kLocationIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kBackIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kForwardIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kReloadIndex) isHidden]); EXPECT_FALSE([GetSubviewAt(kAppMenuIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kHomeIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBrowserActionContainerViewIndex) isHidden]); // Now test the inescapable state. [bar_ setHasToolbar:NO hasLocationBar:YES]; EXPECT_GT([[bar_ view] frame].size.height, 0); EXPECT_EQ([[bar_ view] frame].size.height, [GetSubviewAt(kLocationIndex) frame].size.height); EXPECT_EQ([[bar_ view] frame].size.width, [GetSubviewAt(kLocationIndex) frame].size.width); EXPECT_FALSE([view isHidden]); EXPECT_FALSE([GetSubviewAt(kLocationIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBackIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kForwardIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kReloadIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kAppMenuIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kHomeIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBrowserActionContainerViewIndex) isHidden]); // Maintain visible state. [bar_ updateVisibility:YES withAnimation:NO]; EXPECT_GT([[bar_ view] frame].size.height, 0); EXPECT_EQ([[bar_ view] frame].size.height, [GetSubviewAt(kLocationIndex) frame].size.height); EXPECT_EQ([[bar_ view] frame].size.width, [GetSubviewAt(kLocationIndex) frame].size.width); EXPECT_FALSE([view isHidden]); EXPECT_FALSE([GetSubviewAt(kLocationIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBackIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kForwardIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kReloadIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kAppMenuIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kHomeIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBrowserActionContainerViewIndex) isHidden]); // Hide the toolbar and ensure it has height 0. [bar_ updateVisibility:NO withAnimation:NO]; EXPECT_FALSE([view isHidden]); EXPECT_EQ(0, [resizeDelegate_ height]); EXPECT_EQ(0, [[bar_ view] frame].size.height); // Try to show the home button. [bar_ showOptionalHomeButton]; // Re-show the bar. Buttons should remain hidden, including the home button. [bar_ updateVisibility:YES withAnimation:NO]; EXPECT_GT([resizeDelegate_ height], 0); EXPECT_GT([[bar_ view] frame].size.height, 0); EXPECT_EQ([[bar_ view] frame].size.height, [GetSubviewAt(kLocationIndex) frame].size.height); EXPECT_EQ([[bar_ view] frame].size.width, [GetSubviewAt(kLocationIndex) frame].size.width); EXPECT_FALSE([view isHidden]); EXPECT_FALSE([GetSubviewAt(kLocationIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBackIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kForwardIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kReloadIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kAppMenuIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kHomeIndex) isHidden]); EXPECT_TRUE([GetSubviewAt(kBrowserActionContainerViewIndex) isHidden]); } // Make sure it works in the completely undecorated case. TEST_F(ToolbarControllerTest, NoLocationBar) { NSView* view = [bar_ view]; [bar_ setHasToolbar:NO hasLocationBar:NO]; EXPECT_TRUE([[bar_ view] isHidden]); // Simulate a popup going fullscreen and back by performing the reparenting // that happens during fullscreen transitions NSView* superview = [view superview]; [view removeFromSuperview]; [superview addSubview:view]; } // Make some changes to the enabled state of a few of the buttons and ensure // that we're still in sync. TEST_F(ToolbarControllerTest, UpdateEnabledState) { EXPECT_FALSE(chrome::IsCommandEnabled(browser(), IDC_BACK)); EXPECT_FALSE(chrome::IsCommandEnabled(browser(), IDC_FORWARD)); chrome::UpdateCommandEnabled(browser(), IDC_BACK, true); chrome::UpdateCommandEnabled(browser(), IDC_FORWARD, true); CommandUpdater* updater = browser()->command_controller()->command_updater(); CompareState(updater, [bar_ toolbarViews]); // Change an unwatched command and ensure the last state does not change. updater->UpdateCommandEnabled(IDC_MinimumLabelValue, false); EXPECT_EQ([bar_ lastCommand], IDC_FORWARD); EXPECT_EQ([bar_ lastState], true); } // Focus the location bar and make sure that it's the first responder. TEST_F(ToolbarControllerTest, FocusLocation) { NSWindow* window = test_window(); [window makeFirstResponder:[window contentView]]; EXPECT_EQ([window firstResponder], [window contentView]); [bar_ focusLocationBar:YES]; EXPECT_NE([window firstResponder], [window contentView]); NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex]; EXPECT_EQ([window firstResponder], [(id)locationBar currentEditor]); } TEST_F(ToolbarControllerTest, LoadingState) { // In its initial state, the reload button has a tag of // IDC_RELOAD. When loading, it should be IDC_STOP. NSButton* reload = [[bar_ toolbarViews] objectAtIndex:kReloadIndex]; EXPECT_EQ([reload tag], IDC_RELOAD); [bar_ setIsLoading:YES force:YES]; EXPECT_EQ([reload tag], IDC_STOP); [bar_ setIsLoading:NO force:YES]; EXPECT_EQ([reload tag], IDC_RELOAD); } // Check that toggling the state of the home button changes the visible // state of the home button and moves the other items accordingly. TEST_F(ToolbarControllerTest, ToggleHome) { PrefService* prefs = profile()->GetPrefs(); bool showHome = prefs->GetBoolean(prefs::kShowHomeButton); NSView* homeButton = [[bar_ toolbarViews] objectAtIndex:kHomeIndex]; EXPECT_EQ(showHome, ![homeButton isHidden]); NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex]; NSRect originalLocationBarFrame = [locationBar frame]; // Toggle the pref and make sure the button changed state and the other // views moved. prefs->SetBoolean(prefs::kShowHomeButton, !showHome); EXPECT_EQ(showHome, [homeButton isHidden]); EXPECT_NE(NSMinX(originalLocationBarFrame), NSMinX([locationBar frame])); EXPECT_NE(NSWidth(originalLocationBarFrame), NSWidth([locationBar frame])); } // Ensure that we don't toggle the buttons when we have a strip marked as not // having the full toolbar. Also ensure that the location bar doesn't change // size. TEST_F(ToolbarControllerTest, DontToggleWhenNoToolbar) { [bar_ setHasToolbar:NO hasLocationBar:YES]; NSView* homeButton = [[bar_ toolbarViews] objectAtIndex:kHomeIndex]; NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex]; NSRect locationBarFrame = [locationBar frame]; EXPECT_EQ([homeButton isHidden], YES); [bar_ showOptionalHomeButton]; EXPECT_EQ([homeButton isHidden], YES); NSRect newLocationBarFrame = [locationBar frame]; EXPECT_NSEQ(locationBarFrame, newLocationBarFrame); newLocationBarFrame = [locationBar frame]; EXPECT_NSEQ(locationBarFrame, newLocationBarFrame); } TEST_F(ToolbarControllerTest, BookmarkBubblePoint) { const NSPoint starPoint = [bar_ bookmarkBubblePoint]; const NSRect barFrame = [[bar_ view] convertRect:[[bar_ view] bounds] toView:nil]; // Make sure the star is completely inside the location bar. EXPECT_TRUE(NSPointInRect(starPoint, barFrame)); } TEST_F(ToolbarControllerTest, TranslateBubblePoint) { const NSPoint translatePoint = [bar_ translateBubblePoint]; const NSRect barFrame = [[bar_ view] convertRect:[[bar_ view] bounds] toView:nil]; EXPECT_TRUE(NSPointInRect(translatePoint, barFrame)); } TEST_F(ToolbarControllerTest, HoverButtonForEvent) { base::scoped_nsobject view( [[HitView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]); NSView* toolbarView = [bar_ view]; [bar_ setView:view]; NSEvent* event = [NSEvent mouseEventWithType:NSMouseMoved location:NSMakePoint(10,10) modifierFlags:0 timestamp:0 windowNumber:0 context:nil eventNumber:0 clickCount:0 pressure:0.0]; // NOT a match. [view setHitTestReturn:bar_.get()]; EXPECT_FALSE([bar_ hoverButtonForEvent:event]); // Not yet... base::scoped_nsobject button([[NSButton alloc] init]); [view setHitTestReturn:button]; EXPECT_FALSE([bar_ hoverButtonForEvent:event]); // Now! base::scoped_nsobject cell( [[ImageButtonCell alloc] init]); [button setCell:cell.get()]; EXPECT_TRUE([bar_ hoverButtonForEvent:nil]); // Restore the original view so that // -[ToolbarController browserWillBeDestroyed] will run correctly. [bar_ setView:toolbarView]; } class BrowserRemovedObserver : public chrome::BrowserListObserver { public: BrowserRemovedObserver() { BrowserList::AddObserver(this); } ~BrowserRemovedObserver() override { BrowserList::RemoveObserver(this); } void WaitUntilBrowserRemoved() { run_loop_.Run(); } void OnBrowserRemoved(Browser* browser) override { run_loop_.Quit(); } private: base::RunLoop run_loop_; DISALLOW_COPY_AND_ASSIGN(BrowserRemovedObserver); }; } // namespace