Home | History | Annotate | Download | only in cocoa
      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 
     19 namespace ui {
     20 
     21 namespace {
     22 
     23 const int kTestLabelResourceId = IDS_APP_SCROLLBAR_CXMENU_SCROLLHERE;
     24 
     25 class 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.
     30 class 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.
     84 class 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.
    112 class 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 
    130 TEST_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 
    138 TEST_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 
    163 TEST_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 
    200 TEST_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 
    212 TEST_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 
    232 TEST_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 
    247 void 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 
    256 TEST_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.
    273 TEST_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 
    290 TEST_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.
    311 TEST_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 
    349 TEST_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