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 #import "base/mac/scoped_nsobject.h"
8 #include "base/prefs/pref_service.h"
9 #include "base/run_loop.h"
10 #include "chrome/app/chrome_command_ids.h"
11 #include "chrome/browser/command_updater.h"
12 #include "chrome/browser/ui/browser.h"
13 #include "chrome/browser/ui/browser_command_controller.h"
14 #include "chrome/browser/ui/browser_commands.h"
15 #include "chrome/browser/ui/browser_list.h"
16 #include "chrome/browser/ui/browser_list_observer.h"
17 #include "chrome/browser/ui/cocoa/cocoa_profile_test.h"
18 #import "chrome/browser/ui/cocoa/image_button_cell.h"
19 #import "chrome/browser/ui/cocoa/toolbar/toolbar_controller.h"
20 #include "chrome/common/pref_names.h"
21 #include "chrome/test/base/testing_profile.h"
22 #include "testing/gtest/include/gtest/gtest.h"
23 #import "testing/gtest_mac.h"
24 #include "testing/platform_test.h"
26 // An NSView that fakes out hitTest:.
27 @interface HitView : NSView {
32 @implementation HitView
34 - (void)setHitTestReturn:(id)rtn {
38 - (NSView *)hitTest:(NSPoint)aPoint {
39 return hitTestReturn_;
44 // Records the last command id and enabled state it has received so it can be
45 // queried by the tests to see if we got a notification or not.
46 @interface TestToolbarController : ToolbarController {
48 int lastCommand_; // Id of last received state change.
49 bool lastState_; // State of last received state change.
51 @property(nonatomic, readonly) int lastCommand;
52 @property(nonatomic, readonly) bool lastState;
55 @implementation TestToolbarController
56 @synthesize lastCommand = lastCommand_;
57 @synthesize lastState = lastState_;
58 - (void)enabledStateChangedForCommand:(int)command enabled:(bool)enabled {
59 [super enabledStateChangedForCommand:command enabled:enabled];
60 lastCommand_ = command;
67 class ToolbarControllerTest : public CocoaProfileTest {
70 // Indexes that match the ordering returned by the private ToolbarController
71 // |-toolbarViews| method.
73 kBackIndex, kForwardIndex, kReloadIndex, kHomeIndex,
74 kWrenchIndex, kLocationIndex, kBrowserActionContainerViewIndex
77 void SetUp() override {
78 CocoaProfileTest::SetUp();
79 ASSERT_TRUE(browser());
81 CommandUpdater* updater =
82 browser()->command_controller()->command_updater();
83 // The default state for the commands is true, set a couple to false to
84 // ensure they get picked up correct on initialization
85 updater->UpdateCommandEnabled(IDC_BACK, false);
86 updater->UpdateCommandEnabled(IDC_FORWARD, false);
87 bar_.reset([[TestToolbarController alloc]
88 initWithCommands:browser()->command_controller()->command_updater()
91 EXPECT_TRUE([bar_ view]);
92 NSView* parent = [test_window() contentView];
93 [parent addSubview:[bar_ view]];
96 void TearDown() override {
97 bar_.reset(); // browser() must outlive the ToolbarController.
98 CocoaProfileTest::TearDown();
101 // Make sure the enabled state of the view is the same as the corresponding
102 // command in the updater. The views are in the declaration order of outlets.
103 void CompareState(CommandUpdater* updater, NSArray* views) {
104 EXPECT_EQ(updater->IsCommandEnabled(IDC_BACK),
105 [[views objectAtIndex:kBackIndex] isEnabled] ? true : false);
106 EXPECT_EQ(updater->IsCommandEnabled(IDC_FORWARD),
107 [[views objectAtIndex:kForwardIndex] isEnabled] ? true : false);
108 EXPECT_EQ(updater->IsCommandEnabled(IDC_RELOAD),
109 [[views objectAtIndex:kReloadIndex] isEnabled] ? true : false);
110 EXPECT_EQ(updater->IsCommandEnabled(IDC_HOME),
111 [[views objectAtIndex:kHomeIndex] isEnabled] ? true : false);
114 base::scoped_nsobject<TestToolbarController> bar_;
117 TEST_VIEW(ToolbarControllerTest, [bar_ view])
119 // Test the initial state that everything is sync'd up
120 TEST_F(ToolbarControllerTest, InitialState) {
121 CommandUpdater* updater = browser()->command_controller()->command_updater();
122 CompareState(updater, [bar_ toolbarViews]);
125 // Make sure a "titlebar only" toolbar with location bar works.
126 TEST_F(ToolbarControllerTest, TitlebarOnly) {
127 NSView* view = [bar_ view];
129 [bar_ setHasToolbar:NO hasLocationBar:YES];
130 EXPECT_NE(view, [bar_ view]);
132 // Simulate a popup going fullscreen and back by performing the reparenting
133 // that happens during fullscreen transitions
134 NSView* superview = [view superview];
135 [view removeFromSuperview];
136 [superview addSubview:view];
138 [bar_ setHasToolbar:YES hasLocationBar:YES];
139 EXPECT_EQ(view, [bar_ view]);
141 // Leave it off to make sure that's fine
142 [bar_ setHasToolbar:NO hasLocationBar:YES];
145 // Make sure it works in the completely undecorated case.
146 TEST_F(ToolbarControllerTest, NoLocationBar) {
147 NSView* view = [bar_ view];
149 [bar_ setHasToolbar:NO hasLocationBar:NO];
150 EXPECT_NE(view, [bar_ view]);
151 EXPECT_TRUE([[bar_ view] isHidden]);
153 // Simulate a popup going fullscreen and back by performing the reparenting
154 // that happens during fullscreen transitions
155 NSView* superview = [view superview];
156 [view removeFromSuperview];
157 [superview addSubview:view];
160 // Make some changes to the enabled state of a few of the buttons and ensure
161 // that we're still in sync.
162 TEST_F(ToolbarControllerTest, UpdateEnabledState) {
163 EXPECT_FALSE(chrome::IsCommandEnabled(browser(), IDC_BACK));
164 EXPECT_FALSE(chrome::IsCommandEnabled(browser(), IDC_FORWARD));
165 chrome::UpdateCommandEnabled(browser(), IDC_BACK, true);
166 chrome::UpdateCommandEnabled(browser(), IDC_FORWARD, true);
167 CommandUpdater* updater = browser()->command_controller()->command_updater();
168 CompareState(updater, [bar_ toolbarViews]);
170 // Change an unwatched command and ensure the last state does not change.
171 updater->UpdateCommandEnabled(IDC_MinimumLabelValue, false);
172 EXPECT_EQ([bar_ lastCommand], IDC_FORWARD);
173 EXPECT_EQ([bar_ lastState], true);
176 // Focus the location bar and make sure that it's the first responder.
177 TEST_F(ToolbarControllerTest, FocusLocation) {
178 NSWindow* window = test_window();
179 [window makeFirstResponder:[window contentView]];
180 EXPECT_EQ([window firstResponder], [window contentView]);
181 [bar_ focusLocationBar:YES];
182 EXPECT_NE([window firstResponder], [window contentView]);
183 NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex];
184 EXPECT_EQ([window firstResponder], [(id)locationBar currentEditor]);
187 TEST_F(ToolbarControllerTest, LoadingState) {
188 // In its initial state, the reload button has a tag of
189 // IDC_RELOAD. When loading, it should be IDC_STOP.
190 NSButton* reload = [[bar_ toolbarViews] objectAtIndex:kReloadIndex];
191 EXPECT_EQ([reload tag], IDC_RELOAD);
192 [bar_ setIsLoading:YES force:YES];
193 EXPECT_EQ([reload tag], IDC_STOP);
194 [bar_ setIsLoading:NO force:YES];
195 EXPECT_EQ([reload tag], IDC_RELOAD);
198 // Check that toggling the state of the home button changes the visible
199 // state of the home button and moves the other items accordingly.
200 TEST_F(ToolbarControllerTest, ToggleHome) {
201 PrefService* prefs = profile()->GetPrefs();
202 bool showHome = prefs->GetBoolean(prefs::kShowHomeButton);
203 NSView* homeButton = [[bar_ toolbarViews] objectAtIndex:kHomeIndex];
204 EXPECT_EQ(showHome, ![homeButton isHidden]);
206 NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex];
207 NSRect originalLocationBarFrame = [locationBar frame];
209 // Toggle the pref and make sure the button changed state and the other
211 prefs->SetBoolean(prefs::kShowHomeButton, !showHome);
212 EXPECT_EQ(showHome, [homeButton isHidden]);
213 EXPECT_NE(NSMinX(originalLocationBarFrame), NSMinX([locationBar frame]));
214 EXPECT_NE(NSWidth(originalLocationBarFrame), NSWidth([locationBar frame]));
217 // Ensure that we don't toggle the buttons when we have a strip marked as not
218 // having the full toolbar. Also ensure that the location bar doesn't change
220 TEST_F(ToolbarControllerTest, DontToggleWhenNoToolbar) {
221 [bar_ setHasToolbar:NO hasLocationBar:YES];
222 NSView* homeButton = [[bar_ toolbarViews] objectAtIndex:kHomeIndex];
223 NSView* locationBar = [[bar_ toolbarViews] objectAtIndex:kLocationIndex];
224 NSRect locationBarFrame = [locationBar frame];
225 EXPECT_EQ([homeButton isHidden], YES);
226 [bar_ showOptionalHomeButton];
227 EXPECT_EQ([homeButton isHidden], YES);
228 NSRect newLocationBarFrame = [locationBar frame];
229 EXPECT_NSEQ(locationBarFrame, newLocationBarFrame);
230 newLocationBarFrame = [locationBar frame];
231 EXPECT_NSEQ(locationBarFrame, newLocationBarFrame);
234 TEST_F(ToolbarControllerTest, BookmarkBubblePoint) {
235 const NSPoint starPoint = [bar_ bookmarkBubblePoint];
236 const NSRect barFrame =
237 [[bar_ view] convertRect:[[bar_ view] bounds] toView:nil];
239 // Make sure the star is completely inside the location bar.
240 EXPECT_TRUE(NSPointInRect(starPoint, barFrame));
243 TEST_F(ToolbarControllerTest, TranslateBubblePoint) {
244 const NSPoint translatePoint = [bar_ translateBubblePoint];
245 const NSRect barFrame =
246 [[bar_ view] convertRect:[[bar_ view] bounds] toView:nil];
247 EXPECT_TRUE(NSPointInRect(translatePoint, barFrame));
250 TEST_F(ToolbarControllerTest, HoverButtonForEvent) {
251 base::scoped_nsobject<HitView> view(
252 [[HitView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]);
254 NSEvent* event = [NSEvent mouseEventWithType:NSMouseMoved
255 location:NSMakePoint(10,10)
265 [view setHitTestReturn:bar_.get()];
266 EXPECT_FALSE([bar_ hoverButtonForEvent:event]);
269 base::scoped_nsobject<NSButton> button([[NSButton alloc] init]);
270 [view setHitTestReturn:button];
271 EXPECT_FALSE([bar_ hoverButtonForEvent:event]);
274 base::scoped_nsobject<ImageButtonCell> cell(
275 [[ImageButtonCell alloc] init]);
276 [button setCell:cell.get()];
277 EXPECT_TRUE([bar_ hoverButtonForEvent:nil]);
280 class BrowserRemovedObserver : public chrome::BrowserListObserver {
282 BrowserRemovedObserver() { BrowserList::AddObserver(this); }
283 ~BrowserRemovedObserver() override { BrowserList::RemoveObserver(this); }
284 void WaitUntilBrowserRemoved() { run_loop_.Run(); }
285 void OnBrowserRemoved(Browser* browser) override { run_loop_.Quit(); }
288 base::RunLoop run_loop_;
290 DISALLOW_COPY_AND_ASSIGN(BrowserRemovedObserver);
293 // Test that ToolbarController can be destroyed after the Browser.
294 // This can happen because the ToolbarController is retained by both the
295 // BrowserWindowController and -[ToolbarController view], the latter of which is
297 TEST_F(ToolbarControllerTest, ToolbarDestroyedAfterBrowser) {
298 BrowserRemovedObserver observer;
299 // This is normally called by BrowserWindowController, but since |bar_| is not
300 // owned by one, call it here.
301 [bar_ browserWillBeDestroyed];
302 CloseBrowserWindow();
303 observer.WaitUntilBrowserRemoved();
304 // |bar_| is released in TearDown().