1 // Copyright (c) 2011 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 <AppKit/AppKit.h> 6 7 #import "base/memory/scoped_nsobject.h" 8 #include "base/string16.h" 9 #include "base/string_util.h" 10 #include "base/utf_string_conversions.h" 11 #include "chrome/app/chrome_command_ids.h" 12 #include "chrome/browser/bookmarks/bookmark_model.h" 13 #include "chrome/browser/ui/cocoa/bookmarks/bookmark_menu_bridge.h" 14 #include "chrome/browser/ui/cocoa/browser_test_helper.h" 15 #include "grit/generated_resources.h" 16 #include "testing/gtest/include/gtest/gtest.h" 17 #import "testing/gtest_mac.h" 18 #include "testing/platform_test.h" 19 #include "ui/base/l10n/l10n_util.h" 20 21 class TestBookmarkMenuBridge : public BookmarkMenuBridge { 22 public: 23 TestBookmarkMenuBridge(Profile* profile) 24 : BookmarkMenuBridge(profile), 25 menu_([[NSMenu alloc] initWithTitle:@"test"]) { 26 } 27 virtual ~TestBookmarkMenuBridge() {} 28 29 scoped_nsobject<NSMenu> menu_; 30 31 protected: 32 // Overridden from BookmarkMenuBridge. 33 virtual NSMenu* BookmarkMenu() { 34 return menu_; 35 } 36 }; 37 38 // TODO(jrg): see refactor comment in bookmark_bar_state_controller_unittest.mm 39 class BookmarkMenuBridgeTest : public PlatformTest { 40 public: 41 42 void SetUp() { 43 bridge_.reset(new TestBookmarkMenuBridge(browser_test_helper_.profile())); 44 EXPECT_TRUE(bridge_.get()); 45 } 46 47 // We are a friend of BookmarkMenuBridge (and have access to 48 // protected methods), but none of the classes generated by TEST_F() 49 // are. This (and AddNodeToMenu()) are simple wrappers to let 50 // derived test classes have access to protected methods. 51 void ClearBookmarkMenu(BookmarkMenuBridge* bridge, NSMenu* menu) { 52 bridge->ClearBookmarkMenu(menu); 53 } 54 55 void InvalidateMenu() { bridge_->InvalidateMenu(); } 56 bool menu_is_valid() { return bridge_->menuIsValid_; } 57 58 void AddNodeToMenu(BookmarkMenuBridge* bridge, 59 const BookmarkNode* root, 60 NSMenu* menu) { 61 bridge->AddNodeToMenu(root, menu); 62 } 63 64 void AddItemToMenu(BookmarkMenuBridge* bridge, 65 int command_id, 66 int message_id, 67 const BookmarkNode* node, 68 NSMenu* menu, 69 bool enable) { 70 bridge->AddItemToMenu(command_id, message_id, node, menu, enable); 71 } 72 73 NSMenuItem* MenuItemForNode(BookmarkMenuBridge* bridge, 74 const BookmarkNode* node) { 75 return bridge->MenuItemForNode(node); 76 } 77 78 NSMenuItem* AddTestMenuItem(NSMenu *menu, NSString *title, SEL selector) { 79 NSMenuItem *item = [[[NSMenuItem alloc] initWithTitle:title action:NULL 80 keyEquivalent:@""] autorelease]; 81 if (selector) 82 [item setAction:selector]; 83 [menu addItem:item]; 84 return item; 85 } 86 BrowserTestHelper browser_test_helper_; 87 scoped_ptr<TestBookmarkMenuBridge> bridge_; 88 }; 89 90 TEST_F(BookmarkMenuBridgeTest, TestBookmarkMenuAutoSeparator) { 91 BookmarkModel* model = bridge_->GetBookmarkModel(); 92 bridge_->Loaded(model); 93 NSMenu* menu = bridge_->menu_.get(); 94 bridge_->UpdateMenu(menu); 95 // The bare menu after loading has a separator and an "Other Bookmarks" 96 // submenu. 97 EXPECT_EQ(2, [menu numberOfItems]); 98 // Add a bookmark and reload and there should be 8 items: the previous 99 // menu contents plus two new separator, the new bookmark and three 100 // versions of 'Open All Bookmarks' menu items. 101 const BookmarkNode* parent = model->GetBookmarkBarNode(); 102 const char* url = "http://www.zim-bop-a-dee.com/"; 103 model->AddURL(parent, 0, ASCIIToUTF16("Bookmark"), GURL(url)); 104 bridge_->UpdateMenu(menu); 105 EXPECT_EQ(8, [menu numberOfItems]); 106 // Remove the new bookmark and reload and we should have 2 items again 107 // because the separator should have been removed as well. 108 model->Remove(parent, 0); 109 bridge_->UpdateMenu(menu); 110 EXPECT_EQ(2, [menu numberOfItems]); 111 } 112 113 // Test that ClearBookmarkMenu() removes all bookmark menus. 114 TEST_F(BookmarkMenuBridgeTest, TestClearBookmarkMenu) { 115 NSMenu* menu = bridge_->menu_.get(); 116 117 AddTestMenuItem(menu, @"hi mom", nil); 118 AddTestMenuItem(menu, @"not", @selector(openBookmarkMenuItem:)); 119 NSMenuItem* item = AddTestMenuItem(menu, @"hi mom", nil); 120 [item setSubmenu:[[[NSMenu alloc] initWithTitle:@"bar"] autorelease]]; 121 AddTestMenuItem(menu, @"not", @selector(openBookmarkMenuItem:)); 122 AddTestMenuItem(menu, @"zippy", @selector(length)); 123 [menu addItem:[NSMenuItem separatorItem]]; 124 125 ClearBookmarkMenu(bridge_.get(), menu); 126 127 // Make sure all bookmark items are removed, all items with 128 // submenus removed, and all separator items are gone. 129 EXPECT_EQ(2, [menu numberOfItems]); 130 for (NSMenuItem *item in [menu itemArray]) { 131 EXPECT_NSNE(@"not", [item title]); 132 } 133 } 134 135 // Test invalidation 136 TEST_F(BookmarkMenuBridgeTest, TestInvalidation) { 137 BookmarkModel* model = bridge_->GetBookmarkModel(); 138 bridge_->Loaded(model); 139 140 EXPECT_FALSE(menu_is_valid()); 141 bridge_->UpdateMenu(bridge_->menu_); 142 EXPECT_TRUE(menu_is_valid()); 143 144 InvalidateMenu(); 145 EXPECT_FALSE(menu_is_valid()); 146 InvalidateMenu(); 147 EXPECT_FALSE(menu_is_valid()); 148 bridge_->UpdateMenu(bridge_->menu_); 149 EXPECT_TRUE(menu_is_valid()); 150 bridge_->UpdateMenu(bridge_->menu_); 151 EXPECT_TRUE(menu_is_valid()); 152 153 const BookmarkNode* parent = model->GetBookmarkBarNode(); 154 const char* url = "http://www.zim-bop-a-dee.com/"; 155 model->AddURL(parent, 0, ASCIIToUTF16("Bookmark"), GURL(url)); 156 157 EXPECT_FALSE(menu_is_valid()); 158 bridge_->UpdateMenu(bridge_->menu_); 159 EXPECT_TRUE(menu_is_valid()); 160 } 161 162 // Test that AddNodeToMenu() properly adds bookmark nodes as menus, 163 // including the recursive case. 164 TEST_F(BookmarkMenuBridgeTest, TestAddNodeToMenu) { 165 string16 empty; 166 NSMenu* menu = bridge_->menu_.get(); 167 168 BookmarkModel* model = bridge_->GetBookmarkModel(); 169 const BookmarkNode* root = model->GetBookmarkBarNode(); 170 EXPECT_TRUE(model && root); 171 172 const char* short_url = "http://foo/"; 173 const char* long_url = "http://super-duper-long-url--." 174 "that.cannot.possibly.fit.even-in-80-columns" 175 "or.be.reasonably-displayed-in-a-menu" 176 "without.looking-ridiculous.com/"; // 140 chars total 177 178 // 3 nodes; middle one has a child, last one has a HUGE URL 179 // Set their titles to be the same as the URLs 180 const BookmarkNode* node = NULL; 181 model->AddURL(root, 0, ASCIIToUTF16(short_url), GURL(short_url)); 182 bridge_->UpdateMenu(menu); 183 int prev_count = [menu numberOfItems] - 1; // "extras" added at this point 184 node = model->AddFolder(root, 1, empty); 185 model->AddURL(root, 2, ASCIIToUTF16(long_url), GURL(long_url)); 186 187 // And the submenu fo the middle one 188 model->AddURL(node, 0, empty, GURL("http://sub")); 189 bridge_->UpdateMenu(menu); 190 191 EXPECT_EQ((NSInteger)(prev_count+3), [menu numberOfItems]); 192 193 // Verify the 1st one is there with the right action. 194 NSMenuItem* item = [menu itemWithTitle:[NSString 195 stringWithUTF8String:short_url]]; 196 EXPECT_TRUE(item); 197 EXPECT_EQ(@selector(openBookmarkMenuItem:), [item action]); 198 EXPECT_EQ(NO, [item hasSubmenu]); 199 NSMenuItem* short_item = item; 200 NSMenuItem* long_item = nil; 201 202 // Now confirm we have 2 submenus (the one we added, plus "other") 203 int subs = 0; 204 for (item in [menu itemArray]) { 205 if ([item hasSubmenu]) 206 subs++; 207 } 208 EXPECT_EQ(2, subs); 209 210 for (item in [menu itemArray]) { 211 if ([[item title] hasPrefix:@"http://super-duper"]) { 212 long_item = item; 213 break; 214 } 215 } 216 EXPECT_TRUE(long_item); 217 218 // Make sure a short title looks fine 219 NSString* s = [short_item title]; 220 EXPECT_NSEQ([NSString stringWithUTF8String:short_url], s); 221 222 // Make sure a super-long title gets trimmed 223 s = [long_item title]; 224 EXPECT_TRUE([s length] < strlen(long_url)); 225 226 // Confirm tooltips and confirm they are not trimmed (like the item 227 // name might be). Add tolerance for URL fixer-upping; 228 // e.g. http://foo becomes http://foo/) 229 EXPECT_GE([[short_item toolTip] length], (2*strlen(short_url) - 5)); 230 EXPECT_GE([[long_item toolTip] length], (2*strlen(long_url) - 5)); 231 232 // Make sure the favicon is non-nil (should be either the default site 233 // icon or a favicon, if present). 234 EXPECT_TRUE([short_item image]); 235 EXPECT_TRUE([long_item image]); 236 } 237 238 // Test that AddItemToMenu() properly added versions of 239 // 'Open All Bookmarks' as menu items. 240 TEST_F(BookmarkMenuBridgeTest, TestAddItemToMenu) { 241 NSString* title; 242 NSMenuItem* item; 243 NSMenu* menu = bridge_->menu_.get(); 244 245 BookmarkModel* model = bridge_->GetBookmarkModel(); 246 const BookmarkNode* root = model->GetBookmarkBarNode(); 247 EXPECT_TRUE(model && root); 248 EXPECT_EQ(0, [menu numberOfItems]); 249 250 AddItemToMenu(bridge_.get(), IDC_BOOKMARK_BAR_OPEN_ALL, 251 IDS_BOOMARK_BAR_OPEN_ALL, root, menu, true); 252 AddItemToMenu(bridge_.get(), IDC_BOOKMARK_BAR_OPEN_ALL_NEW_WINDOW, 253 IDS_BOOMARK_BAR_OPEN_ALL_NEW_WINDOW, root, menu, true); 254 AddItemToMenu(bridge_.get(), IDC_BOOKMARK_BAR_OPEN_ALL_INCOGNITO, 255 IDS_BOOMARK_BAR_OPEN_INCOGNITO, root, menu, true); 256 EXPECT_EQ(3, [menu numberOfItems]); 257 258 title = l10n_util::GetNSString(IDS_BOOMARK_BAR_OPEN_ALL); 259 item = [menu itemWithTitle:title]; 260 EXPECT_TRUE(item); 261 EXPECT_EQ(@selector(openAllBookmarks:), [item action]); 262 EXPECT_TRUE([item isEnabled]); 263 264 title = l10n_util::GetNSString(IDS_BOOMARK_BAR_OPEN_ALL_NEW_WINDOW); 265 item = [menu itemWithTitle:title]; 266 EXPECT_TRUE(item); 267 EXPECT_EQ(@selector(openAllBookmarksNewWindow:), [item action]); 268 EXPECT_TRUE([item isEnabled]); 269 270 title = l10n_util::GetNSString(IDS_BOOMARK_BAR_OPEN_INCOGNITO); 271 item = [menu itemWithTitle:title]; 272 EXPECT_TRUE(item); 273 EXPECT_EQ(@selector(openAllBookmarksIncognitoWindow:), [item action]); 274 EXPECT_TRUE([item isEnabled]); 275 276 ClearBookmarkMenu(bridge_.get(), menu); 277 EXPECT_EQ(0, [menu numberOfItems]); 278 279 AddItemToMenu(bridge_.get(), IDC_BOOKMARK_BAR_OPEN_ALL, 280 IDS_BOOMARK_BAR_OPEN_ALL, root, menu, false); 281 AddItemToMenu(bridge_.get(), IDC_BOOKMARK_BAR_OPEN_ALL_NEW_WINDOW, 282 IDS_BOOMARK_BAR_OPEN_ALL_NEW_WINDOW, root, menu, false); 283 AddItemToMenu(bridge_.get(), IDC_BOOKMARK_BAR_OPEN_ALL_INCOGNITO, 284 IDS_BOOMARK_BAR_OPEN_INCOGNITO, root, menu, false); 285 EXPECT_EQ(3, [menu numberOfItems]); 286 287 title = l10n_util::GetNSString(IDS_BOOMARK_BAR_OPEN_ALL); 288 item = [menu itemWithTitle:title]; 289 EXPECT_TRUE(item); 290 EXPECT_EQ(nil, [item action]); 291 EXPECT_FALSE([item isEnabled]); 292 293 title = l10n_util::GetNSString(IDS_BOOMARK_BAR_OPEN_ALL_NEW_WINDOW); 294 item = [menu itemWithTitle:title]; 295 EXPECT_TRUE(item); 296 EXPECT_EQ(nil, [item action]); 297 EXPECT_FALSE([item isEnabled]); 298 299 title = l10n_util::GetNSString(IDS_BOOMARK_BAR_OPEN_INCOGNITO); 300 item = [menu itemWithTitle:title]; 301 EXPECT_TRUE(item); 302 EXPECT_EQ(nil, [item action]); 303 EXPECT_FALSE([item isEnabled]); 304 } 305 306 // Makes sure our internal map of BookmarkNode to NSMenuItem works. 307 TEST_F(BookmarkMenuBridgeTest, TestGetMenuItemForNode) { 308 string16 empty; 309 NSMenu* menu = bridge_->menu_.get(); 310 311 BookmarkModel* model = bridge_->GetBookmarkModel(); 312 const BookmarkNode* bookmark_bar = model->GetBookmarkBarNode(); 313 const BookmarkNode* root = model->AddFolder(bookmark_bar, 0, empty); 314 EXPECT_TRUE(model && root); 315 316 model->AddURL(root, 0, ASCIIToUTF16("Test Item"), GURL("http://test")); 317 AddNodeToMenu(bridge_.get(), root, menu); 318 EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0))); 319 320 model->AddURL(root, 1, ASCIIToUTF16("Test 2"), GURL("http://second-test")); 321 AddNodeToMenu(bridge_.get(), root, menu); 322 EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0))); 323 EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(1))); 324 325 const BookmarkNode* removed_node = root->GetChild(0); 326 EXPECT_EQ(2, root->child_count()); 327 model->Remove(root, 0); 328 EXPECT_EQ(1, root->child_count()); 329 bridge_->UpdateMenu(menu); 330 EXPECT_FALSE(MenuItemForNode(bridge_.get(), removed_node)); 331 EXPECT_TRUE(MenuItemForNode(bridge_.get(), root->GetChild(0))); 332 333 const BookmarkNode empty_node(GURL("http://no-where/")); 334 EXPECT_FALSE(MenuItemForNode(bridge_.get(), &empty_node)); 335 EXPECT_FALSE(MenuItemForNode(bridge_.get(), NULL)); 336 } 337 338 // Test that Loaded() adds both the bookmark bar nodes and the "other" nodes. 339 TEST_F(BookmarkMenuBridgeTest, TestAddNodeToOther) { 340 NSMenu* menu = bridge_->menu_.get(); 341 342 BookmarkModel* model = bridge_->GetBookmarkModel(); 343 const BookmarkNode* root = model->other_node(); 344 EXPECT_TRUE(model && root); 345 346 const char* short_url = "http://foo/"; 347 model->AddURL(root, 0, ASCIIToUTF16(short_url), GURL(short_url)); 348 349 bridge_->UpdateMenu(menu); 350 ASSERT_GT([menu numberOfItems], 0); 351 NSMenuItem* other = [menu itemAtIndex:([menu numberOfItems]-1)]; 352 EXPECT_TRUE(other); 353 EXPECT_TRUE([other hasSubmenu]); 354 ASSERT_GT([[other submenu] numberOfItems], 0); 355 EXPECT_NSEQ(@"http://foo/", [[[other submenu] itemAtIndex:0] title]); 356 } 357 358 TEST_F(BookmarkMenuBridgeTest, TestFaviconLoading) { 359 NSMenu* menu = bridge_->menu_; 360 361 BookmarkModel* model = bridge_->GetBookmarkModel(); 362 const BookmarkNode* root = model->GetBookmarkBarNode(); 363 EXPECT_TRUE(model && root); 364 365 const BookmarkNode* node = 366 model->AddURL(root, 0, ASCIIToUTF16("Test Item"), 367 GURL("http://favicon-test")); 368 bridge_->UpdateMenu(menu); 369 NSMenuItem* item = [menu itemWithTitle:@"Test Item"]; 370 EXPECT_TRUE([item image]); 371 [item setImage:nil]; 372 bridge_->BookmarkNodeFaviconLoaded(model, node); 373 EXPECT_TRUE([item image]); 374 } 375 376 TEST_F(BookmarkMenuBridgeTest, TestChangeTitle) { 377 NSMenu* menu = bridge_->menu_; 378 BookmarkModel* model = bridge_->GetBookmarkModel(); 379 const BookmarkNode* root = model->GetBookmarkBarNode(); 380 EXPECT_TRUE(model && root); 381 382 const BookmarkNode* node = 383 model->AddURL(root, 0, ASCIIToUTF16("Test Item"), 384 GURL("http://title-test")); 385 bridge_->UpdateMenu(menu); 386 NSMenuItem* item = [menu itemWithTitle:@"Test Item"]; 387 EXPECT_TRUE([item image]); 388 389 model->SetTitle(node, ASCIIToUTF16("New Title")); 390 391 item = [menu itemWithTitle:@"Test Item"]; 392 EXPECT_FALSE(item); 393 item = [menu itemWithTitle:@"New Title"]; 394 EXPECT_TRUE(item); 395 } 396 397