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 bool IsCommandIdChecked(int command_id) const override { return false; }
43 bool IsCommandIdEnabled(int command_id) const override {
47 bool GetAcceleratorForCommandId(int command_id,
48 Accelerator* accelerator) override {
51 void ExecuteCommand(int command_id, int event_flags) override {
55 void MenuWillShow(SimpleMenuModel* /*source*/) override {
56 EXPECT_FALSE(did_show_);
57 EXPECT_FALSE(did_close_);
59 NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
62 [menu_to_close_ performSelector:@selector(cancelTracking)
68 void MenuClosed(SimpleMenuModel* /*source*/) override {
69 EXPECT_TRUE(did_show_);
70 EXPECT_FALSE(did_close_);
75 mutable int enable_count_;
76 // The menu on which to call |-cancelTracking| after a short delay in
78 NSMenu* menu_to_close_;
83 // Just like Delegate, except the items are treated as "dynamic" so updates to
84 // the label/icon in the model are reflected in the menu.
85 class DynamicDelegate : public Delegate {
88 bool IsItemForCommandIdDynamic(int command_id) const override { return true; }
89 base::string16 GetLabelForCommandId(int command_id) const override {
92 bool GetIconForCommandId(int command_id, gfx::Image* icon) const override {
93 if (icon_.IsEmpty()) {
100 void SetDynamicLabel(base::string16 label) { label_ = label; }
101 void SetDynamicIcon(const gfx::Image& icon) { icon_ = icon; }
104 base::string16 label_;
108 // Menu model that returns a gfx::FontList object for one of the items in the
110 class FontListMenuModel : public SimpleMenuModel {
112 FontListMenuModel(SimpleMenuModel::Delegate* delegate,
113 const gfx::FontList* font_list, int index)
114 : SimpleMenuModel(delegate),
115 font_list_(font_list),
118 ~FontListMenuModel() override {}
119 const gfx::FontList* GetLabelFontListAt(int index) const override {
120 return (index == index_) ? font_list_ : NULL;
124 const gfx::FontList* font_list_;
128 TEST_F(MenuControllerTest, EmptyMenu) {
130 SimpleMenuModel model(&delegate);
131 base::scoped_nsobject<MenuController> menu(
132 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
133 EXPECT_EQ([[menu menu] numberOfItems], 0);
136 TEST_F(MenuControllerTest, BasicCreation) {
138 SimpleMenuModel model(&delegate);
139 model.AddItem(1, ASCIIToUTF16("one"));
140 model.AddItem(2, ASCIIToUTF16("two"));
141 model.AddItem(3, ASCIIToUTF16("three"));
142 model.AddSeparator(NORMAL_SEPARATOR);
143 model.AddItem(4, ASCIIToUTF16("four"));
144 model.AddItem(5, ASCIIToUTF16("five"));
146 base::scoped_nsobject<MenuController> menu(
147 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
148 EXPECT_EQ([[menu menu] numberOfItems], 6);
150 // Check the title, tag, and represented object are correct for a random
152 NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
153 NSString* title = [itemTwo title];
154 EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
155 EXPECT_EQ([itemTwo tag], 2);
156 EXPECT_EQ([[itemTwo representedObject] pointerValue], &model);
158 EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
161 TEST_F(MenuControllerTest, Submenus) {
163 SimpleMenuModel model(&delegate);
164 model.AddItem(1, ASCIIToUTF16("one"));
165 SimpleMenuModel submodel(&delegate);
166 submodel.AddItem(2, ASCIIToUTF16("sub-one"));
167 submodel.AddItem(3, ASCIIToUTF16("sub-two"));
168 submodel.AddItem(4, ASCIIToUTF16("sub-three"));
169 model.AddSubMenuWithStringId(5, kTestLabelResourceId, &submodel);
170 model.AddItem(6, ASCIIToUTF16("three"));
172 base::scoped_nsobject<MenuController> menu(
173 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
174 EXPECT_EQ([[menu menu] numberOfItems], 3);
176 // Inspect the submenu to ensure it has correct properties.
177 NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
178 EXPECT_TRUE(submenu);
179 EXPECT_EQ([submenu numberOfItems], 3);
181 // Inspect one of the items to make sure it has the correct model as its
182 // represented object and the proper tag.
183 NSMenuItem* submenuItem = [submenu itemAtIndex:1];
184 NSString* title = [submenuItem title];
185 EXPECT_EQ(ASCIIToUTF16("sub-two"), base::SysNSStringToUTF16(title));
186 EXPECT_EQ([submenuItem tag], 1);
187 EXPECT_EQ([[submenuItem representedObject] pointerValue], &submodel);
189 // Make sure the item after the submenu is correct and its represented
190 // object is back to the top model.
191 NSMenuItem* item = [[menu menu] itemAtIndex:2];
192 title = [item title];
193 EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
194 EXPECT_EQ([item tag], 2);
195 EXPECT_EQ([[item representedObject] pointerValue], &model);
198 TEST_F(MenuControllerTest, EmptySubmenu) {
200 SimpleMenuModel model(&delegate);
201 model.AddItem(1, ASCIIToUTF16("one"));
202 SimpleMenuModel submodel(&delegate);
203 model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
205 base::scoped_nsobject<MenuController> menu(
206 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
207 EXPECT_EQ([[menu menu] numberOfItems], 2);
210 TEST_F(MenuControllerTest, PopUpButton) {
212 SimpleMenuModel model(&delegate);
213 model.AddItem(1, ASCIIToUTF16("one"));
214 model.AddItem(2, ASCIIToUTF16("two"));
215 model.AddItem(3, ASCIIToUTF16("three"));
217 // Menu should have an extra item inserted at position 0 that has an empty
219 base::scoped_nsobject<MenuController> menu(
220 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:YES]);
221 EXPECT_EQ([[menu menu] numberOfItems], 4);
222 EXPECT_EQ(base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]),
225 // Make sure the tags are still correct (the index no longer matches the tag).
226 NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
227 EXPECT_EQ([itemTwo tag], 1);
230 TEST_F(MenuControllerTest, Execute) {
232 SimpleMenuModel model(&delegate);
233 model.AddItem(1, ASCIIToUTF16("one"));
234 base::scoped_nsobject<MenuController> menu(
235 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
236 EXPECT_EQ([[menu menu] numberOfItems], 1);
238 // Fake selecting the menu item, we expect the delegate to be told to execute
240 NSMenuItem* item = [[menu menu] itemAtIndex:0];
241 [[item target] performSelector:[item action] withObject:item];
242 EXPECT_EQ(delegate.execute_count_, 1);
245 void Validate(MenuController* controller, NSMenu* menu) {
246 for (int i = 0; i < [menu numberOfItems]; ++i) {
247 NSMenuItem* item = [menu itemAtIndex:i];
248 [controller validateUserInterfaceItem:item];
249 if ([item hasSubmenu])
250 Validate(controller, [item submenu]);
254 TEST_F(MenuControllerTest, Validate) {
256 SimpleMenuModel model(&delegate);
257 model.AddItem(1, ASCIIToUTF16("one"));
258 model.AddItem(2, ASCIIToUTF16("two"));
259 SimpleMenuModel submodel(&delegate);
260 submodel.AddItem(2, ASCIIToUTF16("sub-one"));
261 model.AddSubMenuWithStringId(3, kTestLabelResourceId, &submodel);
263 base::scoped_nsobject<MenuController> menu(
264 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
265 EXPECT_EQ([[menu menu] numberOfItems], 3);
267 Validate(menu.get(), [menu menu]);
270 // Tests that items which have a font set actually use that font.
271 TEST_F(MenuControllerTest, LabelFontList) {
273 const gfx::FontList& bold = ResourceBundle::GetSharedInstance().GetFontList(
274 ResourceBundle::BoldFont);
275 FontListMenuModel model(&delegate, &bold, 0);
276 model.AddItem(1, ASCIIToUTF16("one"));
277 model.AddItem(2, ASCIIToUTF16("two"));
279 base::scoped_nsobject<MenuController> menu(
280 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
281 EXPECT_EQ([[menu menu] numberOfItems], 2);
283 Validate(menu.get(), [menu menu]);
285 EXPECT_TRUE([[[menu menu] itemAtIndex:0] attributedTitle] != nil);
286 EXPECT_TRUE([[[menu menu] itemAtIndex:1] attributedTitle] == nil);
289 TEST_F(MenuControllerTest, DefaultInitializer) {
291 SimpleMenuModel model(&delegate);
292 model.AddItem(1, ASCIIToUTF16("one"));
293 model.AddItem(2, ASCIIToUTF16("two"));
294 model.AddItem(3, ASCIIToUTF16("three"));
296 base::scoped_nsobject<MenuController> menu([[MenuController alloc] init]);
297 EXPECT_FALSE([menu menu]);
299 [menu setModel:&model];
300 [menu setUseWithPopUpButtonCell:NO];
301 EXPECT_TRUE([menu menu]);
302 EXPECT_EQ(3, [[menu menu] numberOfItems]);
304 // Check immutability.
305 model.AddItem(4, ASCIIToUTF16("four"));
306 EXPECT_EQ(3, [[menu menu] numberOfItems]);
309 // Test that menus with dynamic labels actually get updated.
310 TEST_F(MenuControllerTest, Dynamic) {
311 DynamicDelegate delegate;
313 // Create a menu containing a single item whose label is "initial" and who has
315 base::string16 initial = ASCIIToUTF16("initial");
316 delegate.SetDynamicLabel(initial);
317 SimpleMenuModel model(&delegate);
318 model.AddItem(1, ASCIIToUTF16("foo"));
319 base::scoped_nsobject<MenuController> menu(
320 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
321 EXPECT_EQ([[menu menu] numberOfItems], 1);
322 // Validate() simulates opening the menu - the item label/icon should be
323 // initialized after this so we can validate the menu contents.
324 Validate(menu.get(), [menu menu]);
325 NSMenuItem* item = [[menu menu] itemAtIndex:0];
326 // Item should have the "initial" label and no icon.
327 EXPECT_EQ(initial, base::SysNSStringToUTF16([item title]));
328 EXPECT_EQ(nil, [item image]);
330 // Now update the item to have a label of "second" and an icon.
331 base::string16 second = ASCIIToUTF16("second");
332 delegate.SetDynamicLabel(second);
333 const gfx::Image& icon =
334 ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER);
335 delegate.SetDynamicIcon(icon);
336 // Simulate opening the menu and validate that the item label + icon changes.
337 Validate(menu.get(), [menu menu]);
338 EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
339 EXPECT_TRUE([item image] != nil);
341 // Now get rid of the icon and make sure it goes away.
342 delegate.SetDynamicIcon(gfx::Image());
343 Validate(menu.get(), [menu menu]);
344 EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
345 EXPECT_EQ(nil, [item image]);
348 TEST_F(MenuControllerTest, OpenClose) {
349 // SimpleMenuModel posts a task that calls Delegate::MenuClosed. Create
350 // a MessageLoop for that purpose.
351 base::MessageLoopForUI message_loop;
355 SimpleMenuModel model(&delegate);
356 model.AddItem(1, ASCIIToUTF16("allays"));
357 model.AddItem(2, ASCIIToUTF16("i"));
358 model.AddItem(3, ASCIIToUTF16("bf"));
360 // Create the controller.
361 base::scoped_nsobject<MenuController> menu(
362 [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
363 delegate.menu_to_close_ = [menu menu];
365 EXPECT_FALSE([menu isMenuOpen]);
367 // In the event tracking run loop mode of the menu, verify that the controller
368 // resports the menu as open.
369 CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
370 EXPECT_TRUE([menu isMenuOpen]);
373 // Pop open the menu, which will spin an event-tracking run loop.
374 [NSMenu popUpContextMenu:[menu menu]
376 forView:[test_window() contentView]];
378 EXPECT_FALSE([menu isMenuOpen]);
380 // When control returns back to here, the menu will have finished running its
381 // loop and will have closed itself (see Delegate::MenuWillShow).
382 EXPECT_TRUE(delegate.did_show_);
384 // When the menu tells the Model it closed, the Model posts a task to notify
385 // the delegate. But since this is a test and there's no running MessageLoop,
386 // |did_close_| will remain false until we pump the task manually.
387 EXPECT_FALSE(delegate.did_close_);
389 // Pump the task that notifies the delegate.
390 message_loop.RunUntilIdle();
392 // Expect that the delegate got notified properly.
393 EXPECT_TRUE(delegate.did_close_);