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