1 // Copyright 2013 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/message_loop/message_loop.h"
8 #include "base/strings/sys_string_conversions.h"
9 #include "base/strings/utf_string_conversions.h"
10 #include "third_party/skia/include/core/SkBitmap.h"
11 #import "ui/base/cocoa/menu_controller.h"
12 #include "ui/base/models/simple_menu_model.h"
13 #include "ui/base/resource/resource_bundle.h"
14 #include "ui/gfx/image/image.h"
15 #import "ui/gfx/test/ui_cocoa_test_helper.h"
16 #include "ui/resources/grit/ui_resources.h"
17 #include "ui/strings/grit/ui_strings.h"
19 using base::ASCIIToUTF16;
25 const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
27 class MenuControllerTest : public CocoaTest {
30 // A menu delegate that counts the number of times certain things are called
31 // to make sure things are hooked up properly.
32 class Delegate : public SimpleMenuModel::Delegate {
42 virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
45 virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
49 virtual bool GetAcceleratorForCommandId(
51 Accelerator* accelerator) OVERRIDE { return false; }
52 virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
56 virtual void MenuWillShow(SimpleMenuModel* /*source*/) OVERRIDE {
57 EXPECT_FALSE(did_show_);
58 EXPECT_FALSE(did_close_);
60 NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
63 [menu_to_close_ performSelector:@selector(cancelTracking)
69 virtual void MenuClosed(SimpleMenuModel* /*source*/) OVERRIDE {
70 EXPECT_TRUE(did_show_);
71 EXPECT_FALSE(did_close_);
76 mutable int enable_count_;
77 // The menu on which to call |-cancelTracking| after a short delay in
79 NSMenu* menu_to_close_;
84 // Just like Delegate, except the items are treated as "dynamic" so updates to
85 // the label/icon in the model are reflected in the menu.
86 class DynamicDelegate : public Delegate {
89 virtual bool IsItemForCommandIdDynamic(int command_id) const OVERRIDE {
92 virtual base::string16 GetLabelForCommandId(int command_id) const OVERRIDE {
95 virtual bool GetIconForCommandId(
97 gfx::Image* icon) const OVERRIDE {
98 if (icon_.IsEmpty()) {
105 void SetDynamicLabel(base::string16 label) { label_ = label; }
106 void SetDynamicIcon(const gfx::Image& icon) { icon_ = icon; }
109 base::string16 label_;
113 // Menu model that returns a gfx::FontList object for one of the items in the
115 class FontListMenuModel : public SimpleMenuModel {
117 FontListMenuModel(SimpleMenuModel::Delegate* delegate,
118 const gfx::FontList* font_list, int index)
119 : SimpleMenuModel(delegate),
120 font_list_(font_list),
123 virtual ~FontListMenuModel() {}
124 virtual const gfx::FontList* GetLabelFontListAt(int index) const OVERRIDE {
125 return (index == index_) ? font_list_ : NULL;
129 const gfx::FontList* font_list_;
133 TEST_F(MenuControllerTest, EmptyMenu) {
135 SimpleMenuModel model(&delegate);
136 base::scoped_nsobject<MenuController> menu(
137 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
138 EXPECT_EQ([[menu menu] numberOfItems], 0);
141 TEST_F(MenuControllerTest, BasicCreation) {
143 SimpleMenuModel model(&delegate);
144 model.AddItem(1, ASCIIToUTF16("one"));
145 model.AddItem(2, ASCIIToUTF16("two"));
146 model.AddItem(3, ASCIIToUTF16("three"));
147 model.AddSeparator(NORMAL_SEPARATOR);
148 model.AddItem(4, ASCIIToUTF16("four"));
149 model.AddItem(5, ASCIIToUTF16("five"));
151 base::scoped_nsobject<MenuController> menu(
152 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
153 EXPECT_EQ([[menu menu] numberOfItems], 6);
155 // Check the title, tag, and represented object are correct for a random
157 NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
158 NSString* title = [itemTwo title];
159 EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
160 EXPECT_EQ([itemTwo tag], 2);
161 EXPECT_EQ([[itemTwo representedObject] pointerValue], &model);
163 EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
166 TEST_F(MenuControllerTest, Submenus) {
168 SimpleMenuModel model(&delegate);
169 model.AddItem(1, ASCIIToUTF16("one"));
170 SimpleMenuModel submodel(&delegate);
171 submodel.AddItem(2, ASCIIToUTF16("sub-one"));
172 submodel.AddItem(3, ASCIIToUTF16("sub-two"));
173 submodel.AddItem(4, ASCIIToUTF16("sub-three"));
174 model.AddSubMenuWithStringId(5, kTestLabelResourceId, &submodel);
175 model.AddItem(6, ASCIIToUTF16("three"));
177 base::scoped_nsobject<MenuController> menu(
178 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
179 EXPECT_EQ([[menu menu] numberOfItems], 3);
181 // Inspect the submenu to ensure it has correct properties.
182 NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
183 EXPECT_TRUE(submenu);
184 EXPECT_EQ([submenu numberOfItems], 3);
186 // Inspect one of the items to make sure it has the correct model as its
187 // represented object and the proper tag.
188 NSMenuItem* submenuItem = [submenu itemAtIndex:1];
189 NSString* title = [submenuItem title];
190 EXPECT_EQ(ASCIIToUTF16("sub-two"), base::SysNSStringToUTF16(title));
191 EXPECT_EQ([submenuItem tag], 1);
192 EXPECT_EQ([[submenuItem representedObject] pointerValue], &submodel);
194 // Make sure the item after the submenu is correct and its represented
195 // object is back to the top model.
196 NSMenuItem* item = [[menu menu] itemAtIndex:2];
197 title = [item title];
198 EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
199 EXPECT_EQ([item tag], 2);
200 EXPECT_EQ([[item representedObject] pointerValue], &model);
203 TEST_F(MenuControllerTest, EmptySubmenu) {
205 SimpleMenuModel model(&delegate);
206 model.AddItem(1, ASCIIToUTF16("one"));
207 SimpleMenuModel submodel(&delegate);
208 model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
210 base::scoped_nsobject<MenuController> menu(
211 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
212 EXPECT_EQ([[menu menu] numberOfItems], 2);
215 TEST_F(MenuControllerTest, PopUpButton) {
217 SimpleMenuModel model(&delegate);
218 model.AddItem(1, ASCIIToUTF16("one"));
219 model.AddItem(2, ASCIIToUTF16("two"));
220 model.AddItem(3, ASCIIToUTF16("three"));
222 // Menu should have an extra item inserted at position 0 that has an empty
224 base::scoped_nsobject<MenuController> menu(
225 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:YES]);
226 EXPECT_EQ([[menu menu] numberOfItems], 4);
227 EXPECT_EQ(base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]),
230 // Make sure the tags are still correct (the index no longer matches the tag).
231 NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
232 EXPECT_EQ([itemTwo tag], 1);
235 TEST_F(MenuControllerTest, Execute) {
237 SimpleMenuModel model(&delegate);
238 model.AddItem(1, ASCIIToUTF16("one"));
239 base::scoped_nsobject<MenuController> menu(
240 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
241 EXPECT_EQ([[menu menu] numberOfItems], 1);
243 // Fake selecting the menu item, we expect the delegate to be told to execute
245 NSMenuItem* item = [[menu menu] itemAtIndex:0];
246 [[item target] performSelector:[item action] withObject:item];
247 EXPECT_EQ(delegate.execute_count_, 1);
250 void Validate(MenuController* controller, NSMenu* menu) {
251 for (int i = 0; i < [menu numberOfItems]; ++i) {
252 NSMenuItem* item = [menu itemAtIndex:i];
253 [controller validateUserInterfaceItem:item];
254 if ([item hasSubmenu])
255 Validate(controller, [item submenu]);
259 TEST_F(MenuControllerTest, Validate) {
261 SimpleMenuModel model(&delegate);
262 model.AddItem(1, ASCIIToUTF16("one"));
263 model.AddItem(2, ASCIIToUTF16("two"));
264 SimpleMenuModel submodel(&delegate);
265 submodel.AddItem(2, ASCIIToUTF16("sub-one"));
266 model.AddSubMenuWithStringId(3, kTestLabelResourceId, &submodel);
268 base::scoped_nsobject<MenuController> menu(
269 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
270 EXPECT_EQ([[menu menu] numberOfItems], 3);
272 Validate(menu.get(), [menu menu]);
275 // Tests that items which have a font set actually use that font.
276 TEST_F(MenuControllerTest, LabelFontList) {
278 const gfx::FontList& bold = ResourceBundle::GetSharedInstance().GetFontList(
279 ResourceBundle::BoldFont);
280 FontListMenuModel model(&delegate, &bold, 0);
281 model.AddItem(1, ASCIIToUTF16("one"));
282 model.AddItem(2, ASCIIToUTF16("two"));
284 base::scoped_nsobject<MenuController> menu(
285 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
286 EXPECT_EQ([[menu menu] numberOfItems], 2);
288 Validate(menu.get(), [menu menu]);
290 EXPECT_TRUE([[[menu menu] itemAtIndex:0] attributedTitle] != nil);
291 EXPECT_TRUE([[[menu menu] itemAtIndex:1] attributedTitle] == nil);
294 TEST_F(MenuControllerTest, DefaultInitializer) {
296 SimpleMenuModel model(&delegate);
297 model.AddItem(1, ASCIIToUTF16("one"));
298 model.AddItem(2, ASCIIToUTF16("two"));
299 model.AddItem(3, ASCIIToUTF16("three"));
301 base::scoped_nsobject<MenuController> menu([[MenuController alloc] init]);
302 EXPECT_FALSE([menu menu]);
304 [menu setModel:&model];
305 [menu setUseWithPopUpButtonCell:NO];
306 EXPECT_TRUE([menu menu]);
307 EXPECT_EQ(3, [[menu menu] numberOfItems]);
309 // Check immutability.
310 model.AddItem(4, ASCIIToUTF16("four"));
311 EXPECT_EQ(3, [[menu menu] numberOfItems]);
314 // Test that menus with dynamic labels actually get updated.
315 TEST_F(MenuControllerTest, Dynamic) {
316 DynamicDelegate delegate;
318 // Create a menu containing a single item whose label is "initial" and who has
320 base::string16 initial = ASCIIToUTF16("initial");
321 delegate.SetDynamicLabel(initial);
322 SimpleMenuModel model(&delegate);
323 model.AddItem(1, ASCIIToUTF16("foo"));
324 base::scoped_nsobject<MenuController> menu(
325 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
326 EXPECT_EQ([[menu menu] numberOfItems], 1);
327 // Validate() simulates opening the menu - the item label/icon should be
328 // initialized after this so we can validate the menu contents.
329 Validate(menu.get(), [menu menu]);
330 NSMenuItem* item = [[menu menu] itemAtIndex:0];
331 // Item should have the "initial" label and no icon.
332 EXPECT_EQ(initial, base::SysNSStringToUTF16([item title]));
333 EXPECT_EQ(nil, [item image]);
335 // Now update the item to have a label of "second" and an icon.
336 base::string16 second = ASCIIToUTF16("second");
337 delegate.SetDynamicLabel(second);
338 const gfx::Image& icon =
339 ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER);
340 delegate.SetDynamicIcon(icon);
341 // Simulate opening the menu and validate that the item label + icon changes.
342 Validate(menu.get(), [menu menu]);
343 EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
344 EXPECT_TRUE([item image] != nil);
346 // Now get rid of the icon and make sure it goes away.
347 delegate.SetDynamicIcon(gfx::Image());
348 Validate(menu.get(), [menu menu]);
349 EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
350 EXPECT_EQ(nil, [item image]);
353 TEST_F(MenuControllerTest, OpenClose) {
354 // SimpleMenuModel posts a task that calls Delegate::MenuClosed. Create
355 // a MessageLoop for that purpose.
356 base::MessageLoopForUI message_loop;
360 SimpleMenuModel model(&delegate);
361 model.AddItem(1, ASCIIToUTF16("allays"));
362 model.AddItem(2, ASCIIToUTF16("i"));
363 model.AddItem(3, ASCIIToUTF16("bf"));
365 // Create the controller.
366 base::scoped_nsobject<MenuController> menu(
367 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
368 delegate.menu_to_close_ = [menu menu];
370 EXPECT_FALSE([menu isMenuOpen]);
372 // In the event tracking run loop mode of the menu, verify that the controller
373 // resports the menu as open.
374 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
375 EXPECT_TRUE([menu isMenuOpen]);
378 // Pop open the menu, which will spin an event-tracking run loop.
379 [NSMenu popUpContextMenu:[menu menu]
381 forView:[test_window() contentView]];
383 EXPECT_FALSE([menu isMenuOpen]);
385 // When control returns back to here, the menu will have finished running its
386 // loop and will have closed itself (see Delegate::MenuWillShow).
387 EXPECT_TRUE(delegate.did_show_);
389 // When the menu tells the Model it closed, the Model posts a task to notify
390 // the delegate. But since this is a test and there's no running MessageLoop,
391 // |did_close_| will remain false until we pump the task manually.
392 EXPECT_FALSE(delegate.did_close_);
394 // Pump the task that notifies the delegate.
395 message_loop.RunUntilIdle();
397 // Expect that the delegate got notified properly.
398 EXPECT_TRUE(delegate.did_close_);