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/run_loop.h"
8#include "base/strings/sys_string_conversions.h"
9#include "base/strings/utf_string_conversions.h"
10#include "base/test/task_environment.h"
11#import "testing/gtest_mac.h"
12#include "third_party/skia/include/core/SkBitmap.h"
13#import "ui/base/cocoa/menu_controller.h"
14#include "ui/base/l10n/l10n_util_mac.h"
15#include "ui/base/models/simple_menu_model.h"
16#include "ui/base/resource/resource_bundle.h"
17#import "ui/base/test/cocoa_helper.h"
18#include "ui/events/test/cocoa_test_event_utils.h"
19#include "ui/gfx/image/image.h"
20#include "ui/gfx/image/image_unittest_util.h"
21#include "ui/strings/grit/ui_strings.h"
22
23using base::ASCIIToUTF16;
24
25@interface WatchedLifetimeMenuController : MenuControllerCocoa
26@property(assign, nonatomic) BOOL* deallocCalled;
27@end
28
29@implementation WatchedLifetimeMenuController {
30  BOOL* _deallocCalled;
31}
32
33@synthesize deallocCalled = _deallocCalled;
34
35- (void)dealloc {
36  *_deallocCalled = YES;
37  [super dealloc];
38}
39
40@end
41
42namespace ui {
43
44namespace {
45
46const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
47
48class MenuControllerTest : public CocoaTest {};
49
50class TestSimpleMenuModelVisibility : public SimpleMenuModel {
51 public:
52  explicit TestSimpleMenuModelVisibility(SimpleMenuModel::Delegate* delegate)
53      : SimpleMenuModel(delegate) {}
54
55  // SimpleMenuModel:
56  bool IsVisibleAt(int index) const override {
57    return items_[ValidateItemIndex(index)].visible;
58  }
59
60  void SetVisibility(int command_id, bool visible) {
61    int index = SimpleMenuModel::GetIndexOfCommandId(command_id);
62    items_[ValidateItemIndex(index)].visible = visible;
63  }
64
65  void AddItem(int command_id, const base::string16& label) {
66    SimpleMenuModel::AddItem(command_id, label);
67    items_.push_back({true, command_id});
68  }
69
70  void AddSubMenuWithStringId(int command_id, int string_id, MenuModel* model) {
71    SimpleMenuModel::AddSubMenuWithStringId(command_id, string_id, model);
72    items_.push_back({true, command_id});
73  }
74
75 private:
76  struct Item {
77    bool visible;
78    int command_id;
79  };
80
81  typedef std::vector<Item> ItemVector;
82
83  int ValidateItemIndex(int index) const {
84    CHECK_GE(index, 0);
85    CHECK_LT(static_cast<size_t>(index), items_.size());
86    return index;
87  }
88
89  ItemVector items_;
90
91  DISALLOW_COPY_AND_ASSIGN(TestSimpleMenuModelVisibility);
92};
93
94// A menu delegate that counts the number of times certain things are called
95// to make sure things are hooked up properly.
96class Delegate : public SimpleMenuModel::Delegate {
97 public:
98  Delegate() {}
99
100  bool IsCommandIdChecked(int command_id) const override { return false; }
101  bool IsCommandIdEnabled(int command_id) const override {
102    ++enable_count_;
103    return true;
104  }
105  void ExecuteCommand(int command_id, int event_flags) override {
106    ++execute_count_;
107  }
108
109  void OnMenuWillShow(SimpleMenuModel* /*source*/) override {
110    EXPECT_FALSE(did_show_);
111    EXPECT_FALSE(did_close_);
112    did_show_ = true;
113    if (auto_close_) {
114      NSArray* modes = @[ NSEventTrackingRunLoopMode, NSDefaultRunLoopMode ];
115      [menu_to_close_ performSelector:@selector(cancelTracking)
116                           withObject:nil
117                           afterDelay:0.1
118                              inModes:modes];
119    }
120  }
121
122  void MenuClosed(SimpleMenuModel* /*source*/) override {
123    EXPECT_TRUE(did_show_);
124    EXPECT_FALSE(did_close_);
125    DCHECK(!did_close_);
126    did_close_ = true;
127  }
128
129  int execute_count_ = 0;
130  mutable int enable_count_ = 0;
131  // The menu on which to call |-cancelTracking| after a short delay in
132  // OnMenuWillShow.
133  NSMenu* menu_to_close_ = nil;
134  bool did_show_ = false;
135  bool did_close_ = false;
136  bool auto_close_ = true;
137
138 private:
139  DISALLOW_COPY_AND_ASSIGN(Delegate);
140};
141
142// Just like Delegate, except the items are treated as "dynamic" so updates to
143// the label/icon in the model are reflected in the menu.
144class DynamicDelegate : public Delegate {
145 public:
146  DynamicDelegate() {}
147  bool IsItemForCommandIdDynamic(int command_id) const override { return true; }
148  base::string16 GetLabelForCommandId(int command_id) const override {
149    return label_;
150  }
151  bool GetIconForCommandId(int command_id, gfx::Image* icon) const override {
152    if (icon_.IsEmpty()) {
153      return false;
154    } else {
155      *icon = icon_;
156      return true;
157    }
158  }
159  void SetDynamicLabel(base::string16 label) { label_ = label; }
160  void SetDynamicIcon(const gfx::Image& icon) { icon_ = icon; }
161
162 private:
163  base::string16 label_;
164  gfx::Image icon_;
165};
166
167// A SimpleMenuModel::Delegate that owns the MenuControllerCocoa and deletes
168// itself when the command is executed.
169class OwningDelegate : public Delegate {
170 public:
171  OwningDelegate(bool* did_delete, BOOL* did_dealloc)
172      : did_delete_(did_delete), model_(this) {
173    model_.AddItem(1, ASCIIToUTF16("foo"));
174    controller_.reset([[WatchedLifetimeMenuController alloc]
175                 initWithModel:&model_
176        useWithPopUpButtonCell:NO]);
177    [controller_ setDeallocCalled:did_dealloc];
178  }
179
180  MenuControllerCocoa* controller() { return controller_; }
181
182  // Delegate:
183  void ExecuteCommand(int command_id, int event_flags) override {
184    // Although -[MenuControllerCocoa menuDidClose:] has been invoked,
185    // SimpleMenuModel always posts a task to call Delegate::MenuClosed(), to
186    // ensure it happens after the command. It uses a weak pointer to |model_|,
187    // so the task will expire before being run.
188    EXPECT_FALSE(did_close_);
189
190    EXPECT_EQ(0, execute_count_);
191    Delegate::ExecuteCommand(command_id, event_flags);
192    delete this;
193  }
194
195 private:
196  ~OwningDelegate() override {
197    EXPECT_FALSE(*did_delete_);
198    *did_delete_ = true;
199  }
200
201  bool* did_delete_;
202  SimpleMenuModel model_;
203  base::scoped_nsobject<WatchedLifetimeMenuController> controller_;
204
205  DISALLOW_COPY_AND_ASSIGN(OwningDelegate);
206};
207
208// Menu model that returns a gfx::FontList object for one of the items in the
209// menu.
210class FontListMenuModel : public SimpleMenuModel {
211 public:
212  FontListMenuModel(SimpleMenuModel::Delegate* delegate,
213                    const gfx::FontList* font_list,
214                    int index)
215      : SimpleMenuModel(delegate), font_list_(font_list), index_(index) {}
216  ~FontListMenuModel() override {}
217  const gfx::FontList* GetLabelFontListAt(int index) const override {
218    return (index == index_) ? font_list_ : NULL;
219  }
220
221 private:
222  const gfx::FontList* font_list_;
223  const int index_;
224};
225
226TEST_F(MenuControllerTest, EmptyMenu) {
227  Delegate delegate;
228  SimpleMenuModel model(&delegate);
229  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
230               initWithModel:&model
231      useWithPopUpButtonCell:NO]);
232  EXPECT_EQ(0, [[menu menu] numberOfItems]);
233}
234
235TEST_F(MenuControllerTest, BasicCreation) {
236  Delegate delegate;
237  SimpleMenuModel model(&delegate);
238  model.AddItem(1, ASCIIToUTF16("one"));
239  model.AddItem(2, ASCIIToUTF16("two"));
240  model.AddItem(3, ASCIIToUTF16("three"));
241  model.AddSeparator(NORMAL_SEPARATOR);
242  model.AddItem(4, ASCIIToUTF16("four"));
243  model.AddItem(5, ASCIIToUTF16("five"));
244
245  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
246               initWithModel:&model
247      useWithPopUpButtonCell:NO]);
248  EXPECT_EQ(6, [[menu menu] numberOfItems]);
249
250  // Check the title, tag, and represented object are correct for a random
251  // element.
252  NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
253  NSString* title = [itemTwo title];
254  EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
255  EXPECT_EQ(2, [itemTwo tag]);
256
257  EXPECT_TRUE([[[menu menu] itemAtIndex:3] isSeparatorItem]);
258}
259
260TEST_F(MenuControllerTest, Submenus) {
261  Delegate delegate;
262  SimpleMenuModel model(&delegate);
263  model.AddItem(1, ASCIIToUTF16("one"));
264  SimpleMenuModel submodel(&delegate);
265  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
266  submodel.AddItem(3, ASCIIToUTF16("sub-two"));
267  submodel.AddItem(4, ASCIIToUTF16("sub-three"));
268  model.AddSubMenuWithStringId(5, kTestLabelResourceId, &submodel);
269  model.AddItem(6, ASCIIToUTF16("three"));
270
271  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
272               initWithModel:&model
273      useWithPopUpButtonCell:NO]);
274  EXPECT_EQ(3, [[menu menu] numberOfItems]);
275
276  // Inspect the submenu to ensure it has correct properties.
277  NSMenuItem* menuItem = [[menu menu] itemAtIndex:1];
278  EXPECT_TRUE([menuItem isEnabled]);
279  NSMenu* submenu = [menuItem submenu];
280  EXPECT_TRUE(submenu);
281  EXPECT_EQ(3, [submenu numberOfItems]);
282
283  // Inspect one of the items to make sure it has the correct model as its
284  // represented object and the proper tag.
285  NSMenuItem* submenuItem = [submenu itemAtIndex:1];
286  NSString* title = [submenuItem title];
287  EXPECT_EQ(ASCIIToUTF16("sub-two"), base::SysNSStringToUTF16(title));
288  EXPECT_EQ(1, [submenuItem tag]);
289
290  // Make sure the item after the submenu is correct and its represented
291  // object is back to the top model.
292  NSMenuItem* item = [[menu menu] itemAtIndex:2];
293  title = [item title];
294  EXPECT_EQ(ASCIIToUTF16("three"), base::SysNSStringToUTF16(title));
295  EXPECT_EQ(2, [item tag]);
296}
297
298TEST_F(MenuControllerTest, EmptySubmenu) {
299  Delegate delegate;
300  SimpleMenuModel model(&delegate);
301  model.AddItem(1, ASCIIToUTF16("one"));
302  SimpleMenuModel submodel(&delegate);
303  model.AddSubMenuWithStringId(2, kTestLabelResourceId, &submodel);
304
305  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
306               initWithModel:&model
307      useWithPopUpButtonCell:NO]);
308  EXPECT_EQ(2, [[menu menu] numberOfItems]);
309
310  // Inspect the submenu to ensure it has one item labeled "(empty)".
311  NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
312  EXPECT_TRUE(submenu);
313  EXPECT_EQ(1, [submenu numberOfItems]);
314
315  EXPECT_NSEQ(@"(empty)", [[submenu itemAtIndex:0] title]);
316}
317
318// Tests that an empty menu item, "(empty)", is added to a submenu that contains
319// hidden child items.
320TEST_F(MenuControllerTest, EmptySubmenuWhenAllChildItemsAreHidden) {
321  Delegate delegate;
322  TestSimpleMenuModelVisibility model(&delegate);
323  model.AddItem(1, ASCIIToUTF16("one"));
324  TestSimpleMenuModelVisibility submodel(&delegate);
325  // Hide the two child menu items.
326  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
327  submodel.SetVisibility(2, false);
328  submodel.AddItem(3, ASCIIToUTF16("sub-two"));
329  submodel.SetVisibility(3, false);
330  model.AddSubMenuWithStringId(4, kTestLabelResourceId, &submodel);
331
332  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
333               initWithModel:&model
334      useWithPopUpButtonCell:NO]);
335  EXPECT_EQ(2, [[menu menu] numberOfItems]);
336
337  // Inspect the submenu to ensure it has one item labeled "(empty)".
338  NSMenu* submenu = [[[menu menu] itemAtIndex:1] submenu];
339  EXPECT_TRUE(submenu);
340  EXPECT_EQ(1, [submenu numberOfItems]);
341
342  EXPECT_NSEQ(@"(empty)", [[submenu itemAtIndex:0] title]);
343}
344
345// Tests hiding a submenu item. If a submenu item with children is set to
346// hidden, then the submenu should hide.
347TEST_F(MenuControllerTest, HiddenSubmenu) {
348  // SimpleMenuModel posts a task that calls Delegate::MenuClosed.
349  base::test::SingleThreadTaskEnvironment task_environment(
350      base::test::SingleThreadTaskEnvironment::MainThreadType::UI);
351
352  // Create the model.
353  Delegate delegate;
354  TestSimpleMenuModelVisibility model(&delegate);
355  model.AddItem(1, ASCIIToUTF16("one"));
356  TestSimpleMenuModelVisibility submodel(&delegate);
357  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
358  submodel.AddItem(3, ASCIIToUTF16("sub-two"));
359  // Set the submenu to be hidden.
360  model.AddSubMenuWithStringId(4, kTestLabelResourceId, &submodel);
361
362  model.SetVisibility(4, false);
363
364  // Create the controller.
365  base::scoped_nsobject<MenuControllerCocoa> menu_controller(
366      [[MenuControllerCocoa alloc] initWithModel:&model
367                          useWithPopUpButtonCell:NO]);
368  EXPECT_EQ(2, [[menu_controller menu] numberOfItems]);
369  delegate.menu_to_close_ = [menu_controller menu];
370
371  // Show the menu.
372  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
373    EXPECT_TRUE([menu_controller isMenuOpen]);
374    // Ensure that the submenu is hidden.
375    NSMenuItem* item = [[menu_controller menu] itemAtIndex:1];
376    EXPECT_TRUE([item isHidden]);
377  });
378
379  // Pop open the menu, which will spin an event-tracking run loop.
380  [NSMenu popUpContextMenu:[menu_controller menu]
381                 withEvent:cocoa_test_event_utils::RightMouseDownAtPoint(
382                               NSZeroPoint)
383                   forView:[test_window() contentView]];
384
385  EXPECT_FALSE([menu_controller isMenuOpen]);
386
387  // Pump the task that notifies the delegate.
388  base::RunLoop().RunUntilIdle();
389
390  // Expect that the delegate got notified properly.
391  EXPECT_TRUE(delegate.did_close_);
392}
393
394TEST_F(MenuControllerTest, DisabledSubmenu) {
395  // SimpleMenuModel posts a task that calls Delegate::MenuClosed.
396  base::test::SingleThreadTaskEnvironment task_environment(
397      base::test::SingleThreadTaskEnvironment::MainThreadType::UI);
398
399  // Create the model.
400  Delegate delegate;
401  SimpleMenuModel model(&delegate);
402  model.AddItem(1, ASCIIToUTF16("one"));
403  SimpleMenuModel disabled_submodel(&delegate);
404  disabled_submodel.AddItem(2, ASCIIToUTF16("disabled_submodel"));
405  model.AddSubMenuWithStringId(3, kTestLabelResourceId, &disabled_submodel);
406  SimpleMenuModel enabled_submodel(&delegate);
407  enabled_submodel.AddItem(4, ASCIIToUTF16("enabled_submodel"));
408  model.AddSubMenuWithStringId(5, kTestLabelResourceId, &enabled_submodel);
409
410  // Disable the first submenu entry.
411  model.SetEnabledAt(1, false);
412
413  // Create the controller.
414  base::scoped_nsobject<MenuControllerCocoa> menu_controller(
415      [[MenuControllerCocoa alloc] initWithModel:&model
416                          useWithPopUpButtonCell:NO]);
417  delegate.menu_to_close_ = [menu_controller menu];
418
419  // Show the menu.
420  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
421    EXPECT_TRUE([menu_controller isMenuOpen]);
422
423    // Ensure that the disabled submenu is disabled.
424    NSMenuItem* disabled_item = [[menu_controller menu] itemAtIndex:1];
425    EXPECT_FALSE([disabled_item isEnabled]);
426
427    // Ensure that the enabled submenu is enabled.
428    NSMenuItem* enabled_item = [[menu_controller menu] itemAtIndex:2];
429    EXPECT_TRUE([enabled_item isEnabled]);
430  });
431
432  // Pop open the menu, which will spin an event-tracking run loop.
433  [NSMenu popUpContextMenu:[menu_controller menu]
434                 withEvent:cocoa_test_event_utils::RightMouseDownAtPoint(
435                               NSZeroPoint)
436                   forView:[test_window() contentView]];
437  EXPECT_FALSE([menu_controller isMenuOpen]);
438
439  // Pump the task that notifies the delegate.
440  base::RunLoop().RunUntilIdle();
441  // Expect that the delegate got notified properly.
442  EXPECT_TRUE(delegate.did_close_);
443}
444
445TEST_F(MenuControllerTest, PopUpButton) {
446  Delegate delegate;
447  SimpleMenuModel model(&delegate);
448  model.AddItem(1, ASCIIToUTF16("one"));
449  model.AddItem(2, ASCIIToUTF16("two"));
450  model.AddItem(3, ASCIIToUTF16("three"));
451
452  // Menu should have an extra item inserted at position 0 that has an empty
453  // title.
454  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
455               initWithModel:&model
456      useWithPopUpButtonCell:YES]);
457  EXPECT_EQ(4, [[menu menu] numberOfItems]);
458  EXPECT_EQ(base::string16(),
459            base::SysNSStringToUTF16([[[menu menu] itemAtIndex:0] title]));
460
461  // Make sure the tags are still correct (the index no longer matches the tag).
462  NSMenuItem* itemTwo = [[menu menu] itemAtIndex:2];
463  EXPECT_EQ(1, [itemTwo tag]);
464}
465
466TEST_F(MenuControllerTest, Execute) {
467  Delegate delegate;
468  SimpleMenuModel model(&delegate);
469  model.AddItem(1, ASCIIToUTF16("one"));
470  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
471               initWithModel:&model
472      useWithPopUpButtonCell:NO]);
473  EXPECT_EQ(1, [[menu menu] numberOfItems]);
474
475  // Fake selecting the menu item, we expect the delegate to be told to execute
476  // a command.
477  NSMenuItem* item = [[menu menu] itemAtIndex:0];
478  [[item target] performSelector:[item action] withObject:item];
479  EXPECT_EQ(1, delegate.execute_count_);
480}
481
482void Validate(MenuControllerCocoa* controller, NSMenu* menu) {
483  for (int i = 0; i < [menu numberOfItems]; ++i) {
484    NSMenuItem* item = [menu itemAtIndex:i];
485    [controller validateUserInterfaceItem:item];
486    if ([item hasSubmenu])
487      Validate(controller, [item submenu]);
488  }
489}
490
491TEST_F(MenuControllerTest, Validate) {
492  Delegate delegate;
493  SimpleMenuModel model(&delegate);
494  model.AddItem(1, ASCIIToUTF16("one"));
495  model.AddItem(2, ASCIIToUTF16("two"));
496  SimpleMenuModel submodel(&delegate);
497  submodel.AddItem(2, ASCIIToUTF16("sub-one"));
498  model.AddSubMenuWithStringId(3, kTestLabelResourceId, &submodel);
499
500  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
501               initWithModel:&model
502      useWithPopUpButtonCell:NO]);
503  EXPECT_EQ(3, [[menu menu] numberOfItems]);
504
505  Validate(menu.get(), [menu menu]);
506}
507
508// Tests that items which have a font set actually use that font.
509TEST_F(MenuControllerTest, LabelFontList) {
510  Delegate delegate;
511  const gfx::FontList& bold =
512      ResourceBundle::GetSharedInstance().GetFontListWithDelta(
513          0, gfx::Font::NORMAL, gfx::Font::Weight::BOLD);
514  FontListMenuModel model(&delegate, &bold, 0);
515  model.AddItem(1, ASCIIToUTF16("one"));
516  model.AddItem(2, ASCIIToUTF16("two"));
517
518  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
519               initWithModel:&model
520      useWithPopUpButtonCell:NO]);
521  EXPECT_EQ(2, [[menu menu] numberOfItems]);
522
523  Validate(menu.get(), [menu menu]);
524
525  EXPECT_TRUE([[[menu menu] itemAtIndex:0] attributedTitle] != nil);
526  EXPECT_TRUE([[[menu menu] itemAtIndex:1] attributedTitle] == nil);
527}
528
529TEST_F(MenuControllerTest, DefaultInitializer) {
530  Delegate delegate;
531  SimpleMenuModel model(&delegate);
532  model.AddItem(1, ASCIIToUTF16("one"));
533  model.AddItem(2, ASCIIToUTF16("two"));
534  model.AddItem(3, ASCIIToUTF16("three"));
535
536  base::scoped_nsobject<MenuControllerCocoa> menu(
537      [[MenuControllerCocoa alloc] init]);
538  EXPECT_FALSE([menu menu]);
539
540  [menu setModel:&model];
541  [menu setUseWithPopUpButtonCell:NO];
542  EXPECT_TRUE([menu menu]);
543  EXPECT_EQ(3, [[menu menu] numberOfItems]);
544
545  // Check immutability.
546  model.AddItem(4, ASCIIToUTF16("four"));
547  EXPECT_EQ(3, [[menu menu] numberOfItems]);
548}
549
550// Test that menus with dynamic labels actually get updated.
551TEST_F(MenuControllerTest, Dynamic) {
552  DynamicDelegate delegate;
553
554  // Create a menu containing a single item whose label is "initial" and who has
555  // no icon.
556  base::string16 initial = ASCIIToUTF16("initial");
557  delegate.SetDynamicLabel(initial);
558  SimpleMenuModel model(&delegate);
559  model.AddItem(1, ASCIIToUTF16("foo"));
560  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
561               initWithModel:&model
562      useWithPopUpButtonCell:NO]);
563  EXPECT_EQ(1, [[menu menu] numberOfItems]);
564  // Validate() simulates opening the menu - the item label/icon should be
565  // initialized after this so we can validate the menu contents.
566  Validate(menu.get(), [menu menu]);
567  NSMenuItem* item = [[menu menu] itemAtIndex:0];
568  // Item should have the "initial" label and no icon.
569  EXPECT_EQ(initial, base::SysNSStringToUTF16([item title]));
570  EXPECT_EQ(nil, [item image]);
571
572  // Now update the item to have a label of "second" and an icon.
573  base::string16 second = ASCIIToUTF16("second");
574  delegate.SetDynamicLabel(second);
575  const gfx::Image& icon = gfx::test::CreateImage(32, 32);
576  delegate.SetDynamicIcon(icon);
577  // Simulate opening the menu and validate that the item label + icon changes.
578  Validate(menu.get(), [menu menu]);
579  EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
580  EXPECT_TRUE([item image] != nil);
581
582  // Now get rid of the icon and make sure it goes away.
583  delegate.SetDynamicIcon(gfx::Image());
584  Validate(menu.get(), [menu menu]);
585  EXPECT_EQ(second, base::SysNSStringToUTF16([item title]));
586  EXPECT_EQ(nil, [item image]);
587}
588
589TEST_F(MenuControllerTest, OpenClose) {
590  // SimpleMenuModel posts a task that calls Delegate::MenuClosed.
591  base::test::SingleThreadTaskEnvironment task_environment(
592      base::test::SingleThreadTaskEnvironment::MainThreadType::UI);
593
594  // Create the model.
595  Delegate delegate;
596  SimpleMenuModel model(&delegate);
597  model.AddItem(1, ASCIIToUTF16("allays"));
598  model.AddItem(2, ASCIIToUTF16("i"));
599  model.AddItem(3, ASCIIToUTF16("bf"));
600
601  // Create the controller.
602  base::scoped_nsobject<MenuControllerCocoa> menu([[MenuControllerCocoa alloc]
603               initWithModel:&model
604      useWithPopUpButtonCell:NO]);
605  delegate.menu_to_close_ = [menu menu];
606
607  EXPECT_FALSE([menu isMenuOpen]);
608
609  // In the event tracking run loop mode of the menu, verify that the controller
610  // resports the menu as open.
611  CFRunLoopPerformBlock(CFRunLoopGetCurrent(), NSEventTrackingRunLoopMode, ^{
612    EXPECT_TRUE([menu isMenuOpen]);
613  });
614
615  // Pop open the menu, which will spin an event-tracking run loop.
616  [NSMenu popUpContextMenu:[menu menu]
617                 withEvent:cocoa_test_event_utils::RightMouseDownAtPoint(
618                               NSZeroPoint)
619                   forView:[test_window() contentView]];
620
621  EXPECT_FALSE([menu isMenuOpen]);
622
623  // When control returns back to here, the menu will have finished running its
624  // loop and will have closed itself (see Delegate::OnMenuWillShow).
625  EXPECT_TRUE(delegate.did_show_);
626
627  // When the menu tells the Model it closed, the Model posts a task to notify
628  // the delegate. But since this is a test and there's no running MessageLoop,
629  // |did_close_| will remain false until we pump the task manually.
630  EXPECT_FALSE(delegate.did_close_);
631
632  // Pump the task that notifies the delegate.
633  base::RunLoop().RunUntilIdle();
634
635  // Expect that the delegate got notified properly.
636  EXPECT_TRUE(delegate.did_close_);
637}
638
639// Tests invoking a menu action on a delegate that immediately releases the
640// MenuControllerCocoa and destroys itself. Note this usually needs asan to
641// actually crash (before it was fixed).
642TEST_F(MenuControllerTest, OwningDelegate) {
643  base::test::SingleThreadTaskEnvironment task_environment(
644      base::test::SingleThreadTaskEnvironment::MainThreadType::UI);
645  bool did_delete = false;
646  BOOL did_dealloc = NO;
647  OwningDelegate* delegate;
648  NSMenuItem* item;
649
650  // The final action is a task posted to the runloop, which drains the
651  // autorelease pool, so ensure that happens in the test.
652  @autoreleasepool {
653    delegate = new OwningDelegate(&did_delete, &did_dealloc);  // Self deleting.
654    delegate->auto_close_ = false;
655
656    // Unretained reference to the controller.
657    MenuControllerCocoa* controller = delegate->controller();
658
659    item = [[controller menu] itemAtIndex:0];
660    EXPECT_TRUE(item);
661
662    // Simulate opening the menu and selecting an item. Without setting
663    // -setPostItemSelectedAsTask:YES, methods are always invoked by AppKit in
664    // the following order.
665    [controller menuWillOpen:[controller menu]];
666    [controller menuDidClose:[controller menu]];
667  }
668  EXPECT_FALSE(did_dealloc);
669  EXPECT_FALSE(did_delete);
670
671  // On 10.15+, [NSMenuItem target] indirectly causes an extra
672  // retain+autorelease of the target. That avoids bugs caused by the
673  // NSMenuItem's action causing destruction of the target, but also causes the
674  // NSMenuItem to get cleaned up later than this test expects. Deal with that
675  // by creating an explicit autorelease pool here.
676  @autoreleasepool {
677    [[item target] performSelector:[item action] withObject:item];
678  }
679  EXPECT_TRUE(did_dealloc);
680  EXPECT_TRUE(did_delete);
681}
682
683}  // namespace
684
685}  // namespace ui
686