• 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 "grit/ui_resources.h"
11#include "grit/ui_strings.h"
12#include "third_party/skia/include/core/SkBitmap.h"
13#import "ui/base/cocoa/menu_controller.h"
14#include "ui/base/models/simple_menu_model.h"
15#include "ui/base/resource/resource_bundle.h"
16#import "ui/base/test/ui_cocoa_test_helper.h"
17#include "ui/gfx/image/image.h"
18
19namespace ui {
20
21namespace {
22
23const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
24
25class MenuControllerTest : public CocoaTest {
26};
27
28// A menu delegate that counts the number of times certain things are called
29// to make sure things are hooked up properly.
30class Delegate : public SimpleMenuModel::Delegate {
31 public:
32  Delegate()
33      : execute_count_(0),
34        enable_count_(0),
35        menu_to_close_(nil),
36        did_show_(false),
37        did_close_(false) {
38  }
39
40  virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
41    return false;
42  }
43  virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
44    ++enable_count_;
45    return true;
46  }
47  virtual bool GetAcceleratorForCommandId(
48      int command_id,
49      Accelerator* accelerator) OVERRIDE { return false; }
50  virtual void ExecuteCommand(int command_id, int event_flags) OVERRIDE {
51    ++execute_count_;
52  }
53
54  virtual void MenuWillShow(SimpleMenuModel* /*source*/) OVERRIDE {
55    EXPECT_FALSE(did_show_);
56    EXPECT_FALSE(did_close_);
57    did_show_ = true;
58    NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
59                                               NSDefaultRunLoopMode,
60                                               nil];
61    [menu_to_close_ performSelector:@selector(cancelTracking)
62                         withObject:nil
63                         afterDelay:0.1
64                            inModes:modes];
65  }
66
67  virtual void MenuClosed(SimpleMenuModel* /*source*/) OVERRIDE {
68    EXPECT_TRUE(did_show_);
69    EXPECT_FALSE(did_close_);
70    did_close_ = true;
71  }
72
73  int execute_count_;
74  mutable int enable_count_;
75  // The menu on which to call |-cancelTracking| after a short delay in
76  // MenuWillShow.
77  NSMenu* menu_to_close_;
78  bool did_show_;
79  bool did_close_;
80};
81
82// Just like Delegate, except the items are treated as "dynamic" so updates to
83// the label/icon in the model are reflected in the menu.
84class DynamicDelegate : public Delegate {
85 public:
86  DynamicDelegate() {}
87  virtual bool IsItemForCommandIdDynamic(int command_id) const OVERRIDE {
88    return true;
89  }
90  virtual string16 GetLabelForCommandId(int command_id) const OVERRIDE {
91    return label_;
92  }
93  virtual bool GetIconForCommandId(
94      int command_id,
95      gfx::Image* icon) const OVERRIDE {
96    if (icon_.IsEmpty()) {
97      return false;
98    } else {
99      *icon = icon_;
100      return true;
101    }
102  }
103  void SetDynamicLabel(string16 label) { label_ = label; }
104  void SetDynamicIcon(const gfx::Image& icon) { icon_ = icon; }
105
106 private:
107  string16 label_;
108  gfx::Image icon_;
109};
110
111// Menu model that returns a gfx::Font object for one of the items in the menu.
112class FontMenuModel : public SimpleMenuModel {
113 public:
114  FontMenuModel(SimpleMenuModel::Delegate* delegate,
115                const gfx::Font* font, int index)
116      : SimpleMenuModel(delegate),
117        font_(font),
118        index_(index) {
119  }
120  virtual ~FontMenuModel() {}
121  virtual const gfx::Font* GetLabelFontAt(int index) const OVERRIDE {
122    return (index == index_) ? font_ : NULL;
123  }
124
125 private:
126  const gfx::Font* font_;
127  const int index_;
128};
129
130TEST_F(MenuControllerTest, EmptyMenu) {
131  Delegate delegate;
132  SimpleMenuModel model(&delegate);
133  base::scoped_nsobject<MenuController> menu(
134      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
135  EXPECT_EQ([[menu menu] numberOfItems], 0);
136}
137
138TEST_F(MenuControllerTest, BasicCreation) {
139  Delegate delegate;
140  SimpleMenuModel model(&delegate);
141  model.AddItem(1, ASCIIToUTF16("one"));
142  model.AddItem(2, ASCIIToUTF16("two"));
143  model.AddItem(3, ASCIIToUTF16("three"));
144  model.AddSeparator(NORMAL_SEPARATOR);
145  model.AddItem(4, ASCIIToUTF16("four"));
146  model.AddItem(5, ASCIIToUTF16("five"));
147
148  base::scoped_nsobject<MenuController> menu(
149      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
150  EXPECT_EQ([[menu menu] numberOfItems], 6);
151
152  // Check the title, tag, and represented object are correct for a random
153  // element.
154  NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
155  NSString* title = [itemTwo title];
156  EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
157  EXPECT_EQ([itemTwo tag], 2);
158  EXPECT_EQ([[itemTwo representedObject] pointerValue], &model);
159
160  EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
161}
162
163TEST_F(MenuControllerTest, Submenus) {
164  Delegate delegate;
165  SimpleMenuModel model(&delegate);
166  model.AddItem(1, ASCIIToUTF16("one"));
167  SimpleMenuModel submodel(&delegate);
168  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
169  submodel.AddItem(3, ASCIIToUTF16("sub-two"));
170  submodel.AddItem(4, ASCIIToUTF16("sub-three"));
171  model.AddSubMenuWithStringId(5, kTestLabelResourceId, &submodel);
172  model.AddItem(6, ASCIIToUTF16("three"));
173
174  base::scoped_nsobject<MenuController> menu(
175      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
176  EXPECT_EQ([[menu menu] numberOfItems], 3);
177
178  // Inspect the submenu to ensure it has correct properties.
179  NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
180  EXPECT_TRUE(submenu);
181  EXPECT_EQ([submenu numberOfItems], 3);
182
183  // Inspect one of the items to make sure it has the correct model as its
184  // represented object and the proper tag.
185  NSMenuItem* submenuItem = [submenu itemAtIndex:1];
186  NSString* title = [submenuItem title];
187  EXPECT_EQ(ASCIIToUTF16("sub-two"), base::SysNSStringToUTF16(title));
188  EXPECT_EQ([submenuItem tag], 1);
189  EXPECT_EQ([[submenuItem representedObject] pointerValue], &submodel);
190
191  // Make sure the item after the submenu is correct and its represented
192  // object is back to the top model.
193  NSMenuItem* item = [[menu menu] itemAtIndex:2];
194  title = [item title];
195  EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
196  EXPECT_EQ([item tag], 2);
197  EXPECT_EQ([[item representedObject] pointerValue], &model);
198}
199
200TEST_F(MenuControllerTest, EmptySubmenu) {
201  Delegate delegate;
202  SimpleMenuModel model(&delegate);
203  model.AddItem(1, ASCIIToUTF16("one"));
204  SimpleMenuModel submodel(&delegate);
205  model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
206
207  base::scoped_nsobject<MenuController> menu(
208      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
209  EXPECT_EQ([[menu menu] numberOfItems], 2);
210}
211
212TEST_F(MenuControllerTest, PopUpButton) {
213  Delegate delegate;
214  SimpleMenuModel model(&delegate);
215  model.AddItem(1, ASCIIToUTF16("one"));
216  model.AddItem(2, ASCIIToUTF16("two"));
217  model.AddItem(3, ASCIIToUTF16("three"));
218
219  // Menu should have an extra item inserted at position 0 that has an empty
220  // title.
221  base::scoped_nsobject<MenuController> menu(
222      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:YES]);
223  EXPECT_EQ([[menu menu] numberOfItems], 4);
224  EXPECT_EQ(base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]),
225            string16());
226
227  // Make sure the tags are still correct (the index no longer matches the tag).
228  NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
229  EXPECT_EQ([itemTwo tag], 1);
230}
231
232TEST_F(MenuControllerTest, Execute) {
233  Delegate delegate;
234  SimpleMenuModel model(&delegate);
235  model.AddItem(1, ASCIIToUTF16("one"));
236  base::scoped_nsobject<MenuController> menu(
237      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
238  EXPECT_EQ([[menu menu] numberOfItems], 1);
239
240  // Fake selecting the menu item, we expect the delegate to be told to execute
241  // a command.
242  NSMenuItem* item = [[menu menu] itemAtIndex:0];
243  [[item target] performSelector:[item action] withObject:item];
244  EXPECT_EQ(delegate.execute_count_, 1);
245}
246
247void Validate(MenuController* controller, NSMenu* menu) {
248  for (int i = 0; i < [menu numberOfItems]; ++i) {
249    NSMenuItem* item = [menu itemAtIndex:i];
250    [controller validateUserInterfaceItem:item];
251    if ([item hasSubmenu])
252      Validate(controller, [item submenu]);
253  }
254}
255
256TEST_F(MenuControllerTest, Validate) {
257  Delegate delegate;
258  SimpleMenuModel model(&delegate);
259  model.AddItem(1, ASCIIToUTF16("one"));
260  model.AddItem(2, ASCIIToUTF16("two"));
261  SimpleMenuModel submodel(&delegate);
262  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
263  model.AddSubMenuWithStringId(3, kTestLabelResourceId, &submodel);
264
265  base::scoped_nsobject<MenuController> menu(
266      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
267  EXPECT_EQ([[menu menu] numberOfItems], 3);
268
269  Validate(menu.get(), [menu menu]);
270}
271
272// Tests that items which have a font set actually use that font.
273TEST_F(MenuControllerTest, LabelFont) {
274  Delegate delegate;
275  gfx::Font bold = (gfx::Font()).DeriveFont(0, gfx::Font::BOLD);
276  FontMenuModel model(&delegate, &bold, 0);
277  model.AddItem(1, ASCIIToUTF16("one"));
278  model.AddItem(2, ASCIIToUTF16("two"));
279
280  base::scoped_nsobject<MenuController> menu(
281      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
282  EXPECT_EQ([[menu menu] numberOfItems], 2);
283
284  Validate(menu.get(), [menu menu]);
285
286  EXPECT_TRUE([[[menu menu] itemAtIndex:0] attributedTitle] != nil);
287  EXPECT_TRUE([[[menu menu] itemAtIndex:1] attributedTitle] == nil);
288}
289
290TEST_F(MenuControllerTest, DefaultInitializer) {
291  Delegate delegate;
292  SimpleMenuModel model(&delegate);
293  model.AddItem(1, ASCIIToUTF16("one"));
294  model.AddItem(2, ASCIIToUTF16("two"));
295  model.AddItem(3, ASCIIToUTF16("three"));
296
297  base::scoped_nsobject<MenuController> menu([[MenuController alloc] init]);
298  EXPECT_FALSE([menu menu]);
299
300  [menu setModel:&model];
301  [menu setUseWithPopUpButtonCell:NO];
302  EXPECT_TRUE([menu menu]);
303  EXPECT_EQ(3, [[menu menu] numberOfItems]);
304
305  // Check immutability.
306  model.AddItem(4, ASCIIToUTF16("four"));
307  EXPECT_EQ(3, [[menu menu] numberOfItems]);
308}
309
310// Test that menus with dynamic labels actually get updated.
311TEST_F(MenuControllerTest, Dynamic) {
312  DynamicDelegate delegate;
313
314  // Create a menu containing a single item whose label is "initial" and who has
315  // no icon.
316  string16 initial = ASCIIToUTF16("initial");
317  delegate.SetDynamicLabel(initial);
318  SimpleMenuModel model(&delegate);
319  model.AddItem(1, ASCIIToUTF16("foo"));
320  base::scoped_nsobject<MenuController> menu(
321      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
322  EXPECT_EQ([[menu menu] numberOfItems], 1);
323  // Validate() simulates opening the menu - the item label/icon should be
324  // initialized after this so we can validate the menu contents.
325  Validate(menu.get(), [menu menu]);
326  NSMenuItem* item = [[menu menu] itemAtIndex:0];
327  // Item should have the "initial" label and no icon.
328  EXPECT_EQ(initial, base::SysNSStringToUTF16([item title]));
329  EXPECT_EQ(nil, [item image]);
330
331  // Now update the item to have a label of "second" and an icon.
332  string16 second = ASCIIToUTF16("second");
333  delegate.SetDynamicLabel(second);
334  const gfx::Image& icon =
335      ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER);
336  delegate.SetDynamicIcon(icon);
337  // Simulate opening the menu and validate that the item label + icon changes.
338  Validate(menu.get(), [menu menu]);
339  EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
340  EXPECT_TRUE([item image] != nil);
341
342  // Now get rid of the icon and make sure it goes away.
343  delegate.SetDynamicIcon(gfx::Image());
344  Validate(menu.get(), [menu menu]);
345  EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
346  EXPECT_EQ(nil, [item image]);
347}
348
349TEST_F(MenuControllerTest, OpenClose) {
350  // SimpleMenuModel posts a task that calls Delegate::MenuClosed. Create
351  // a MessageLoop for that purpose.
352  base::MessageLoop message_loop(base::MessageLoop::TYPE_UI);
353
354  // Create the model.
355  Delegate delegate;
356  SimpleMenuModel model(&delegate);
357  model.AddItem(1, ASCIIToUTF16("allays"));
358  model.AddItem(2, ASCIIToUTF16("i"));
359  model.AddItem(3, ASCIIToUTF16("bf"));
360
361  // Create the controller.
362  base::scoped_nsobject<MenuController> menu(
363      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
364  delegate.menu_to_close_ = [menu menu];
365
366  EXPECT_FALSE([menu isMenuOpen]);
367
368  // In the event tracking run loop mode of the menu, verify that the controller
369  // resports the menu as open.
370  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
371      EXPECT_TRUE([menu isMenuOpen]);
372  });
373
374  // Pop open the menu, which will spin an event-tracking run loop.
375  [NSMenu popUpContextMenu:[menu menu]
376                 withEvent:nil
377                   forView:[test_window() contentView]];
378
379  EXPECT_FALSE([menu isMenuOpen]);
380
381  // When control returns back to here, the menu will have finished running its
382  // loop and will have closed itself (see Delegate::MenuWillShow).
383  EXPECT_TRUE(delegate.did_show_);
384
385  // When the menu tells the Model it closed, the Model posts a task to notify
386  // the delegate. But since this is a test and there's no running MessageLoop,
387  // |did_close_| will remain false until we pump the task manually.
388  EXPECT_FALSE(delegate.did_close_);
389
390  // Pump the task that notifies the delegate.
391  message_loop.RunUntilIdle();
392
393  // Expect that the delegate got notified properly.
394  EXPECT_TRUE(delegate.did_close_);
395}
396
397}  // namespace
398
399}  // namespace ui
400