summaryrefslogtreecommitdiffstats
path: root/chrome/browser/ui/cocoa/menu_controller_unittest.mm
blob: efca488abc8aa305e817ea07eb4405807423ab51 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import <Cocoa/Cocoa.h>

#include "base/message_loop.h"
#include "base/sys_string_conversions.h"
#include "base/utf_string_conversions.h"
#include "chrome/browser/ui/cocoa/cocoa_test_helper.h"
#include "chrome/browser/ui/cocoa/menu_controller.h"
#include "grit/generated_resources.h"
#include "grit/ui_resources.h"
#include "third_party/skia/include/core/SkBitmap.h"
#include "ui/base/models/simple_menu_model.h"
#include "ui/base/resource/resource_bundle.h"
#include "ui/gfx/image/image.h"

namespace {

class MenuControllerTest : public CocoaTest {
};

// A menu delegate that counts the number of times certain things are called
// to make sure things are hooked up properly.
class Delegate : public ui::SimpleMenuModel::Delegate {
 public:
  Delegate()
      : execute_count_(0),
        enable_count_(0),
        menu_to_close_(nil),
        did_show_(false),
        did_close_(false) {
  }

  virtual bool IsCommandIdChecked(int command_id) const OVERRIDE {
    return false;
  }
  virtual bool IsCommandIdEnabled(int command_id) const OVERRIDE {
    ++enable_count_;
    return true;
  }
  virtual bool GetAcceleratorForCommandId(
      int command_id,
      ui::Accelerator* accelerator) OVERRIDE { return false; }
  virtual void ExecuteCommand(int command_id) OVERRIDE { ++execute_count_; }

  virtual void MenuWillShow(ui::SimpleMenuModel* /*source*/) OVERRIDE {
    EXPECT_FALSE(did_show_);
    EXPECT_FALSE(did_close_);
    did_show_ = true;
    NSArray* modes = [NSArray arrayWithObjects:NSEventTrackingRunLoopMode,
                                               NSDefaultRunLoopMode,
                                               nil];
    [menu_to_close_ performSelector:@selector(cancelTracking)
                         withObject:nil
                         afterDelay:0.1
                            inModes:modes];
  }

  virtual void MenuClosed(ui::SimpleMenuModel* /*source*/) OVERRIDE {
    EXPECT_TRUE(did_show_);
    EXPECT_FALSE(did_close_);
    did_close_ = true;
  }

  int execute_count_;
  mutable int enable_count_;
  // The menu on which to call |-cancelTracking| after a short delay in
  // MenuWillShow.
  NSMenu* menu_to_close_;
  bool did_show_;
  bool did_close_;
};

// Just like Delegate, except the items are treated as "dynamic" so updates to
// the label/icon in the model are reflected in the menu.
class DynamicDelegate : public Delegate {
 public:
  DynamicDelegate() {}
  virtual bool IsItemForCommandIdDynamic(int command_id) const OVERRIDE {
    return true;
  }
  virtual string16 GetLabelForCommandId(int command_id) const OVERRIDE {
    return label_;
  }
  virtual bool GetIconForCommandId(
      int command_id,
      gfx::Image* icon) const OVERRIDE {
    if (icon_.IsEmpty()) {
      return false;
    } else {
      *icon = icon_;
      return true;
    }
  }
  void SetDynamicLabel(string16 label) { label_ = label; }
  void SetDynamicIcon(const gfx::Image& icon) { icon_ = icon; }

 private:
  string16 label_;
  gfx::Image icon_;
};

TEST_F(MenuControllerTest, EmptyMenu) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  EXPECT_EQ([[menu menu] numberOfItems], 0);
}

TEST_F(MenuControllerTest, BasicCreation) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("one"));
  model.AddItem(2, ASCIIToUTF16("two"));
  model.AddItem(3, ASCIIToUTF16("three"));
  model.AddSeparator(ui::NORMAL_SEPARATOR);
  model.AddItem(4, ASCIIToUTF16("four"));
  model.AddItem(5, ASCIIToUTF16("five"));

  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  EXPECT_EQ([[menu menu] numberOfItems], 6);

  // Check the title, tag, and represented object are correct for a random
  // element.
  NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
  NSString* title = [itemTwo title];
  EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
  EXPECT_EQ([itemTwo tag], 2);
  EXPECT_EQ([[itemTwo representedObject] pointerValue], &model);

  EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
}

TEST_F(MenuControllerTest, Submenus) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("one"));
  ui::SimpleMenuModel submodel(&delegate);
  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
  submodel.AddItem(3, ASCIIToUTF16("sub-two"));
  submodel.AddItem(4, ASCIIToUTF16("sub-three"));
  model.AddSubMenuWithStringId(5, IDS_ZOOM_MENU, &submodel);
  model.AddItem(6, ASCIIToUTF16("three"));

  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  EXPECT_EQ([[menu menu] numberOfItems], 3);

  // Inspect the submenu to ensure it has correct properties.
  NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
  EXPECT_TRUE(submenu);
  EXPECT_EQ([submenu numberOfItems], 3);

  // Inspect one of the items to make sure it has the correct model as its
  // represented object and the proper tag.
  NSMenuItem* submenuItem = [submenu itemAtIndex:1];
  NSString* title = [submenuItem title];
  EXPECT_EQ(ASCIIToUTF16("sub-two"), base::SysNSStringToUTF16(title));
  EXPECT_EQ([submenuItem tag], 1);
  EXPECT_EQ([[submenuItem representedObject] pointerValue], &submodel);

  // Make sure the item after the submenu is correct and its represented
  // object is back to the top model.
  NSMenuItem* item = [[menu menu] itemAtIndex:2];
  title = [item title];
  EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
  EXPECT_EQ([item tag], 2);
  EXPECT_EQ([[item representedObject] pointerValue], &model);
}

TEST_F(MenuControllerTest, EmptySubmenu) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("one"));
  ui::SimpleMenuModel submodel(&delegate);
  model.AddSubMenuWithStringId(2, IDS_ZOOM_MENU, &submodel);

  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  EXPECT_EQ([[menu menu] numberOfItems], 2);
}

TEST_F(MenuControllerTest, PopUpButton) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("one"));
  model.AddItem(2, ASCIIToUTF16("two"));
  model.AddItem(3, ASCIIToUTF16("three"));

  // Menu should have an extra item inserted at position 0 that has an empty
  // title.
  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:YES]);
  EXPECT_EQ([[menu menu] numberOfItems], 4);
  EXPECT_EQ(base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]),
            string16());

  // Make sure the tags are still correct (the index no longer matches the tag).
  NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
  EXPECT_EQ([itemTwo tag], 1);
}

TEST_F(MenuControllerTest, Execute) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("one"));
  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  EXPECT_EQ([[menu menu] numberOfItems], 1);

  // Fake selecting the menu item, we expect the delegate to be told to execute
  // a command.
  NSMenuItem* item = [[menu menu] itemAtIndex:0];
  [[item target] performSelector:[item action] withObject:item];
  EXPECT_EQ(delegate.execute_count_, 1);
}

void Validate(MenuController* controller, NSMenu* menu) {
  for (int i = 0; i < [menu numberOfItems]; ++i) {
    NSMenuItem* item = [menu itemAtIndex:i];
    [controller validateUserInterfaceItem:item];
    if ([item hasSubmenu])
      Validate(controller, [item submenu]);
  }
}

TEST_F(MenuControllerTest, Validate) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("one"));
  model.AddItem(2, ASCIIToUTF16("two"));
  ui::SimpleMenuModel submodel(&delegate);
  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
  model.AddSubMenuWithStringId(3, IDS_ZOOM_MENU, &submodel);

  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  EXPECT_EQ([[menu menu] numberOfItems], 3);

  Validate(menu.get(), [menu menu]);
}

TEST_F(MenuControllerTest, DefaultInitializer) {
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("one"));
  model.AddItem(2, ASCIIToUTF16("two"));
  model.AddItem(3, ASCIIToUTF16("three"));

  scoped_nsobject<MenuController> menu([[MenuController alloc] init]);
  EXPECT_FALSE([menu menu]);

  [menu setModel:&model];
  [menu setUseWithPopUpButtonCell:NO];
  EXPECT_TRUE([menu menu]);
  EXPECT_EQ(3, [[menu menu] numberOfItems]);

  // Check immutability.
  model.AddItem(4, ASCIIToUTF16("four"));
  EXPECT_EQ(3, [[menu menu] numberOfItems]);
}

// Test that menus with dynamic labels actually get updated.
TEST_F(MenuControllerTest, Dynamic) {
  DynamicDelegate delegate;

  // Create a menu containing a single item whose label is "initial" and who has
  // no icon.
  string16 initial = ASCIIToUTF16("initial");
  delegate.SetDynamicLabel(initial);
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("foo"));
  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  EXPECT_EQ([[menu menu] numberOfItems], 1);
  // Validate() simulates opening the menu - the item label/icon should be
  // initialized after this so we can validate the menu contents.
  Validate(menu.get(), [menu menu]);
  NSMenuItem* item = [[menu menu] itemAtIndex:0];
  // Item should have the "initial" label and no icon.
  EXPECT_EQ(initial, base::SysNSStringToUTF16([item title]));
  EXPECT_EQ(nil, [item image]);

  // Now update the item to have a label of "second" and an icon.
  string16 second = ASCIIToUTF16("second");
  delegate.SetDynamicLabel(second);
  const gfx::Image& icon =
      ResourceBundle::GetSharedInstance().GetNativeImageNamed(IDR_THROBBER);
  delegate.SetDynamicIcon(icon);
  // Simulate opening the menu and validate that the item label + icon changes.
  Validate(menu.get(), [menu menu]);
  EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
  EXPECT_TRUE([item image] != nil);

  // Now get rid of the icon and make sure it goes away.
  delegate.SetDynamicIcon(gfx::Image());
  Validate(menu.get(), [menu menu]);
  EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
  EXPECT_EQ(nil, [item image]);
}

TEST_F(MenuControllerTest, OpenClose) {
  // ui::SimpleMenuModel posts a task that calls Delegate::MenuClosed. Create
  // a MessageLoop for that purpose.
  MessageLoop message_loop(MessageLoop::TYPE_UI);

  // Create the model.
  Delegate delegate;
  ui::SimpleMenuModel model(&delegate);
  model.AddItem(1, ASCIIToUTF16("allays"));
  model.AddItem(2, ASCIIToUTF16("i"));
  model.AddItem(3, ASCIIToUTF16("bf"));

  // Create the controller.
  scoped_nsobject<MenuController> menu(
      [[MenuController alloc] initWithModel:&model useWithPopUpButtonCell:NO]);
  delegate.menu_to_close_ = [menu menu];

  // Pop open the menu, which will spin an event-tracking run loop.
  [NSMenu popUpContextMenu:[menu menu]
                 withEvent:nil
                   forView:[test_window() contentView]];

  // When control returns back to here, the menu will have finished running its
  // loop and will have closed itself (see Delegate::MenuWillShow).
  EXPECT_TRUE(delegate.did_show_);

  // When the menu tells the Model it closed, the Model posts a task to notify
  // the delegate. But since this is a test and there's no running MessageLoop,
  // |did_close_| will remain false until we pump the task manually.
  EXPECT_FALSE(delegate.did_close_);

  // Pump the task that notifies the delegate.
  message_loop.RunUntilIdle();

  // Expect that the delegate got notified properly.
  EXPECT_TRUE(delegate.did_close_);
}

}  // namespace