1 // Copyright 2014 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 "chrome/browser/ui/cocoa/profiles/avatar_menu_bubble_controller.h"
7 #include "base/command_line.h"
8 #include "base/mac/scoped_nsobject.h"
9 #include "base/memory/scoped_ptr.h"
10 #include "base/message_loop/message_pump_mac.h"
11 #include "base/strings/utf_string_conversions.h"
12 #include "chrome/browser/profiles/avatar_menu.h"
13 #include "chrome/browser/profiles/avatar_menu_observer.h"
14 #include "chrome/browser/profiles/profile_info_cache.h"
15 #import "chrome/browser/ui/cocoa/cocoa_test_helper.h"
16 #include "chrome/test/base/testing_browser_process.h"
17 #include "chrome/test/base/testing_profile_manager.h"
18 #include "components/signin/core/common/profile_management_switches.h"
19 #include "components/syncable_prefs/pref_service_syncable.h"
20 #include "content/public/test/test_browser_thread_bundle.h"
21 #include "testing/gtest_mac.h"
22 #import "ui/base/cocoa/controls/hyperlink_button_cell.h"
23 #include "ui/events/test/cocoa_test_event_utils.h"
25 class AvatarMenuBubbleControllerTest : public CocoaTest {
27 AvatarMenuBubbleControllerTest()
28 : manager_(TestingBrowserProcess::GetGlobal()) {
31 void SetUp() override {
32 switches::DisableNewAvatarMenuForTesting(
33 base::CommandLine::ForCurrentProcess());
36 ASSERT_TRUE(manager_.SetUp());
38 manager_.CreateTestingProfile(
39 "test1", scoped_ptr<syncable_prefs::PrefServiceSyncable>(),
40 base::ASCIIToUTF16("Test 1"), 1, std::string(),
41 TestingProfile::TestingFactories());
42 manager_.CreateTestingProfile(
43 "test2", scoped_ptr<syncable_prefs::PrefServiceSyncable>(),
44 base::ASCIIToUTF16("Test 2"), 0, std::string(),
45 TestingProfile::TestingFactories());
47 menu_ = new AvatarMenu(manager_.profile_info_cache(), NULL, NULL);
50 NSRect frame = [test_window() frame];
51 NSPoint point = NSMakePoint(NSMidX(frame), NSMidY(frame));
53 [[AvatarMenuBubbleController alloc] initWithMenu:menu()
54 parentWindow:test_window()
58 TestingProfileManager* manager() { return &manager_; }
59 AvatarMenuBubbleController* controller() { return controller_; }
60 AvatarMenu* menu() { return menu_; }
62 AvatarMenuItemController* GetHighlightedItem() {
63 for (AvatarMenuItemController* item in [controller() items]) {
64 if ([item isHighlighted])
71 content::TestBrowserThreadBundle thread_bundle_;
72 TestingProfileManager manager_;
74 // Weak; releases self.
75 AvatarMenuBubbleController* controller_;
77 // Weak; owned by |controller_|.
81 TEST_F(AvatarMenuBubbleControllerTest, InitialLayout) {
82 [controller() showWindow:nil];
84 // Two profiles means two item views and the new button with separator.
85 NSView* contents = [[controller() window] contentView];
86 EXPECT_EQ(4U, [[contents subviews] count]);
88 // Loop over the items and match the viewController views to subviews.
89 NSMutableArray* subviews =
90 [NSMutableArray arrayWithArray:[contents subviews]];
91 for (AvatarMenuItemController* viewController in [controller() items]) {
92 for (NSView* subview in subviews) {
93 if ([viewController view] == subview) {
94 [subviews removeObject:subview];
100 // The one remaining subview should be the new user button.
101 EXPECT_EQ(2U, [subviews count]);
104 BOOL hasSeparator = NO;
105 for (NSView* subview in subviews) {
106 if ([subview isKindOfClass:[NSButton class]]) {
107 EXPECT_FALSE(hasButton);
110 NSButton* button = static_cast<NSButton*>(subview);
111 EXPECT_EQ(@selector(newProfile:), [button action]);
112 EXPECT_EQ(controller(), [button target]);
113 EXPECT_TRUE([[button cell] isKindOfClass:[HyperlinkButtonCell class]]);
114 } else if ([subview isKindOfClass:[NSBox class]]) {
115 EXPECT_FALSE(hasSeparator);
118 EXPECT_FALSE(subview) << "Unexpected subview: "
119 << [[subview description] UTF8String];
123 [controller() close];
126 TEST_F(AvatarMenuBubbleControllerTest, PerformLayout) {
127 [controller() showWindow:nil];
129 NSView* contents = [[controller() window] contentView];
130 EXPECT_EQ(4U, [[contents subviews] count]);
132 base::scoped_nsobject<NSMutableArray> oldItems([[controller() items] copy]);
134 // Now create a new profile and notify the delegate.
135 manager()->CreateTestingProfile(
136 "test3", scoped_ptr<syncable_prefs::PrefServiceSyncable>(),
137 base::ASCIIToUTF16("Test 3"), 0, std::string(),
138 TestingProfile::TestingFactories());
140 // Testing the bridge is not worth the effort...
141 [controller() performLayout];
143 EXPECT_EQ(5U, [[contents subviews] count]);
145 // Make sure that none of the old items exit.
146 NSArray* newItems = [controller() items];
147 for (AvatarMenuItemController* oldVC in oldItems.get()) {
148 EXPECT_FALSE([newItems containsObject:oldVC]);
149 EXPECT_FALSE([[contents subviews] containsObject:[oldVC view]]);
152 [controller() close];
155 // This subclass is used to inject a delegate into the hide/show edit link
157 @interface TestingAvatarMenuItemController : AvatarMenuItemController
158 <NSAnimationDelegate> {
160 scoped_ptr<base::MessagePumpNSRunLoop> pump_;
162 // After calling |-highlightForEventType:| an animation will possibly be
163 // started. Since the animation is non-blocking, the run loop will need to be
164 // spun (via the MessagePump) until the animation has finished.
165 - (void)runMessagePump;
168 @implementation TestingAvatarMenuItemController
169 - (void)runMessagePump {
171 pump_.reset(new base::MessagePumpNSRunLoop);
175 - (void)willStartAnimation:(NSAnimation*)anim {
176 [anim setDelegate:self];
179 - (void)animationDidEnd:(NSAnimation*)anim {
180 [super animationDidEnd:anim];
184 - (void)animationDidStop:(NSAnimation*)anim {
185 [super animationDidStop:anim];
186 FAIL() << "Animation stopped before it completed its run";
190 - (void)sendHighlightMessageForMouseExited {
191 [self highlightForEventType:NSMouseExited];
192 // Quit the pump because the animation was cancelled before it even ran.
197 TEST_F(AvatarMenuBubbleControllerTest, HighlightForEventType) {
198 base::scoped_nsobject<TestingAvatarMenuItemController> item(
199 [[TestingAvatarMenuItemController alloc] initWithMenuIndex:0
200 menuController:nil]);
201 // Test non-active states first.
202 [[item activeView] setHidden:YES];
204 NSView* editButton = [item editButton];
205 NSView* emailField = [item emailField];
207 // The edit link remains hidden.
208 [item setIsHighlighted:YES];
209 EXPECT_TRUE(editButton.isHidden);
210 EXPECT_FALSE(emailField.isHidden);
212 [item setIsHighlighted:NO];
213 EXPECT_TRUE(editButton.isHidden);
214 EXPECT_FALSE(emailField.isHidden);
216 // Make the item "active" and re-test.
217 [[item activeView] setHidden:NO];
219 [item setIsHighlighted:YES];
220 [item runMessagePump];
222 EXPECT_FALSE(editButton.isHidden);
223 EXPECT_TRUE(emailField.isHidden);
225 [item setIsHighlighted:NO];
226 [item runMessagePump];
228 EXPECT_TRUE(editButton.isHidden);
229 EXPECT_FALSE(emailField.isHidden);
231 // Now mouse over and out quickly, as if scrubbing through the menu, to test
232 // the hover dwell delay.
233 [item highlightForEventType:NSMouseEntered];
234 [item performSelector:@selector(sendHighlightMessageForMouseExited)
237 [item runMessagePump];
239 EXPECT_TRUE(editButton.isHidden);
240 EXPECT_FALSE(emailField.isHidden);
243 TEST_F(AvatarMenuBubbleControllerTest, DownArrow) {
244 EXPECT_NSEQ(nil, GetHighlightedItem());
247 cocoa_test_event_utils::KeyEventWithCharacter(NSDownArrowFunctionKey);
248 // Going down with no item selected should start the selection at the first
250 [controller() keyDown:event];
251 EXPECT_EQ([[controller() items] objectAtIndex:1], GetHighlightedItem());
253 [controller() keyDown:event];
254 EXPECT_EQ([[controller() items] objectAtIndex:0], GetHighlightedItem());
256 // There are no more items now so going down should stay at the last item.
257 [controller() keyDown:event];
258 EXPECT_EQ([[controller() items] objectAtIndex:0], GetHighlightedItem());
261 TEST_F(AvatarMenuBubbleControllerTest, UpArrow) {
262 EXPECT_NSEQ(nil, GetHighlightedItem());
265 cocoa_test_event_utils::KeyEventWithCharacter(NSUpArrowFunctionKey);
266 // Going up with no item selected should start the selection at the last
268 [controller() keyDown:event];
269 EXPECT_EQ([[controller() items] objectAtIndex:0], GetHighlightedItem());
271 [controller() keyDown:event];
272 EXPECT_EQ([[controller() items] objectAtIndex:1], GetHighlightedItem());
274 // There are no more items now so going up should stay at the first item.
275 [controller() keyDown:event];
276 EXPECT_EQ([[controller() items] objectAtIndex:1], GetHighlightedItem());