• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.
4
5#import <Cocoa/Cocoa.h>
6
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"
18
19using base::ASCIIToUTF16;
20
21namespace ui {
22
23namespace {
24
25const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
26
27class MenuControllerTest : public CocoaTest {
28};
29
30// A menu delegate that counts the number of times certain things are called
31// to make sure things are hooked up properly.
32class Delegate : public SimpleMenuModel::Delegate {
33 public:
34  Delegate()
35      : execute_count_(0),
36        enable_count_(0),
37        menu_to_close_(nil),
38        did_show_(false),
39        did_close_(false) {
40  }
41
42  virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
43    return false;
44  }
45  virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
46    ++enable_count_;
47    return true;
48  }
49  virtual bool GetAcceleratorForCommandId(
50      int command_id,
51      Accelerator* accelerator) OVERRIDE { return false; }
52  virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
53    ++execute_count_;
54  }
55
56  virtual void MenuWillShow(SimpleMenuModel* /*source*/) OVERRIDE {
57    EXPECT_FALSE(did_show_);
58    EXPECT_FALSE(did_close_);
59    did_show_ = true;
60    NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
61                                               NSDefaultRunLoopMode,
62                                               nil];
63    [menu_to_close_ performSelector:@selector(cancelTracking)
64                         withObject:nil
65                         afterDelay:0.1
66                            inModes:modes];
67  }
68
69  virtual void MenuClosed(SimpleMenuModel* /*source*/) OVERRIDE {
70    EXPECT_TRUE(did_show_);
71    EXPECT_FALSE(did_close_);
72    did_close_ = true;
73  }
74
75  int execute_count_;
76  mutable int enable_count_;
77  // The menu on which to call |-cancelTracking| after a short delay in
78  // MenuWillShow.
79  NSMenu* menu_to_close_;
80  bool did_show_;
81  bool did_close_;
82};
83
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.
86class DynamicDelegate : public Delegate {
87 public:
88  DynamicDelegate() {}
89  virtual bool IsItemForCommandIdDynamic(int command_id) const OVERRIDE {
90    return true;
91  }
92  virtual base::string16 GetLabelForCommandId(int command_id) const OVERRIDE {
93    return label_;
94  }
95  virtual bool GetIconForCommandId(
96      int command_id,
97      gfx::Image* icon) const OVERRIDE {
98    if (icon_.IsEmpty()) {
99      return false;
100    } else {
101      *icon = icon_;
102      return true;
103    }
104  }
105  void SetDynamicLabel(base::string16 label) { label_ = label; }
106  void SetDynamicIcon(const gfx::Image& icon) { icon_ = icon; }
107
108 private:
109  base::string16 label_;
110  gfx::Image icon_;
111};
112
113// Menu model that returns a gfx::FontList object for one of the items in the
114// menu.
115class FontListMenuModel : public SimpleMenuModel {
116 public:
117  FontListMenuModel(SimpleMenuModel::Delegate* delegate,
118                    const gfx::FontList* font_list, int index)
119      : SimpleMenuModel(delegate),
120        font_list_(font_list),
121        index_(index) {
122  }
123  virtual ~FontListMenuModel() {}
124  virtual const gfx::FontList* GetLabelFontListAt(int index) const OVERRIDE {
125    return (index == index_) ? font_list_ : NULL;
126  }
127
128 private:
129  const gfx::FontList* font_list_;
130  const int index_;
131};
132
133TEST_F(MenuControllerTest, EmptyMenu) {
134  Delegate delegate;
135  SimpleMenuModel model(&delegate);
136  base::scoped_nsobject<MenuController> menu(
137      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
138  EXPECT_EQ([[menu menu] numberOfItems], 0);
139}
140
141TEST_F(MenuControllerTest, BasicCreation) {
142  Delegate delegate;
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"));
150
151  base::scoped_nsobject<MenuController> menu(
152      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
153  EXPECT_EQ([[menu menu] numberOfItems], 6);
154
155  // Check the title, tag, and represented object are correct for a random
156  // element.
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);
162
163  EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
164}
165
166TEST_F(MenuControllerTest, Submenus) {
167  Delegate delegate;
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"));
176
177  base::scoped_nsobject<MenuController> menu(
178      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
179  EXPECT_EQ([[menu menu] numberOfItems], 3);
180
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);
185
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);
193
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);
201}
202
203TEST_F(MenuControllerTest, EmptySubmenu) {
204  Delegate delegate;
205  SimpleMenuModel model(&delegate);
206  model.AddItem(1, ASCIIToUTF16("one"));
207  SimpleMenuModel submodel(&delegate);
208  model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
209
210  base::scoped_nsobject<MenuController> menu(
211      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
212  EXPECT_EQ([[menu menu] numberOfItems], 2);
213}
214
215TEST_F(MenuControllerTest, PopUpButton) {
216  Delegate delegate;
217  SimpleMenuModel model(&delegate);
218  model.AddItem(1, ASCIIToUTF16("one"));
219  model.AddItem(2, ASCIIToUTF16("two"));
220  model.AddItem(3, ASCIIToUTF16("three"));
221
222  // Menu should have an extra item inserted at position 0 that has an empty
223  // title.
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]),
228            base::string16());
229
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);
233}
234
235TEST_F(MenuControllerTest, Execute) {
236  Delegate delegate;
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);
242
243  // Fake selecting the menu item, we expect the delegate to be told to execute
244  // a command.
245  NSMenuItem* item = [[menu menu] itemAtIndex:0];
246  [[item target] performSelector:[item action] withObject:item];
247  EXPECT_EQ(delegate.execute_count_, 1);
248}
249
250void 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]);
256  }
257}
258
259TEST_F(MenuControllerTest, Validate) {
260  Delegate delegate;
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);
267
268  base::scoped_nsobject<MenuController> menu(
269      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
270  EXPECT_EQ([[menu menu] numberOfItems], 3);
271
272  Validate(menu.get(), [menu menu]);
273}
274
275// Tests that items which have a font set actually use that font.
276TEST_F(MenuControllerTest, LabelFontList) {
277  Delegate delegate;
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"));
283
284  base::scoped_nsobject<MenuController> menu(
285      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
286  EXPECT_EQ([[menu menu] numberOfItems], 2);
287
288  Validate(menu.get(), [menu menu]);
289
290  EXPECT_TRUE([[[menu menu] itemAtIndex:0] attributedTitle] != nil);
291  EXPECT_TRUE([[[menu menu] itemAtIndex:1] attributedTitle] == nil);
292}
293
294TEST_F(MenuControllerTest, DefaultInitializer) {
295  Delegate delegate;
296  SimpleMenuModel model(&delegate);
297  model.AddItem(1, ASCIIToUTF16("one"));
298  model.AddItem(2, ASCIIToUTF16("two"));
299  model.AddItem(3, ASCIIToUTF16("three"));
300
301  base::scoped_nsobject<MenuController> menu([[MenuController alloc] init]);
302  EXPECT_FALSE([menu menu]);
303
304  [menu setModel:&model];
305  [menu setUseWithPopUpButtonCell:NO];
306  EXPECT_TRUE([menu menu]);
307  EXPECT_EQ(3, [[menu menu] numberOfItems]);
308
309  // Check immutability.
310  model.AddItem(4, ASCIIToUTF16("four"));
311  EXPECT_EQ(3, [[menu menu] numberOfItems]);
312}
313
314// Test that menus with dynamic labels actually get updated.
315TEST_F(MenuControllerTest, Dynamic) {
316  DynamicDelegate delegate;
317
318  // Create a menu containing a single item whose label is "initial" and who has
319  // no icon.
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]);
334
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);
345
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]);
351}
352
353TEST_F(MenuControllerTest, OpenClose) {
354  // SimpleMenuModel posts a task that calls Delegate::MenuClosed. Create
355  // a MessageLoop for that purpose.
356  base::MessageLoopForUI message_loop;
357
358  // Create the model.
359  Delegate delegate;
360  SimpleMenuModel model(&delegate);
361  model.AddItem(1, ASCIIToUTF16("allays"));
362  model.AddItem(2, ASCIIToUTF16("i"));
363  model.AddItem(3, ASCIIToUTF16("bf"));
364
365  // Create the controller.
366  base::scoped_nsobject<MenuController> menu(
367      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
368  delegate.menu_to_close_ = [menu menu];
369
370  EXPECT_FALSE([menu isMenuOpen]);
371
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]);
376  });
377
378  // Pop open the menu, which will spin an event-tracking run loop.
379  [NSMenu popUpContextMenu:[menu menu]
380                 withEvent:nil
381                   forView:[test_window() contentView]];
382
383  EXPECT_FALSE([menu isMenuOpen]);
384
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_);
388
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_);
393
394  // Pump the task that notifies the delegate.
395  message_loop.RunUntilIdle();
396
397  // Expect that the delegate got notified properly.
398  EXPECT_TRUE(delegate.did_close_);
399}
400
401}  // namespace
402
403}  // namespace ui
404