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