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 <Cocoa/Cocoa.h> 6 #include <vector> 7 8 #include "base/memory/ref_counted_memory.h" 9 #include "base/string_util.h" 10 #include "base/sys_string_conversions.h" 11 #include "base/utf_string_conversions.h" 12 #include "chrome/app/chrome_command_ids.h" 13 #include "chrome/browser/sessions/tab_restore_service.h" 14 #include "chrome/browser/ui/cocoa/browser_test_helper.h" 15 #include "chrome/browser/ui/cocoa/cocoa_test_helper.h" 16 #include "chrome/browser/ui/cocoa/history_menu_bridge.h" 17 #include "content/browser/cancelable_request.h" 18 #include "testing/gmock/include/gmock/gmock.h" 19 #include "testing/gtest/include/gtest/gtest.h" 20 #import "testing/gtest_mac.h" 21 #include "ui/gfx/codec/png_codec.h" 22 23 namespace { 24 25 class MockTRS : public TabRestoreService { 26 public: 27 MockTRS(Profile* profile) : TabRestoreService(profile, NULL) {} 28 MOCK_CONST_METHOD0(entries, const TabRestoreService::Entries&()); 29 }; 30 31 class MockBridge : public HistoryMenuBridge { 32 public: 33 MockBridge(Profile* profile) 34 : HistoryMenuBridge(profile), 35 menu_([[NSMenu alloc] initWithTitle:@"History"]) {} 36 37 virtual NSMenu* HistoryMenu() { 38 return menu_.get(); 39 } 40 41 private: 42 scoped_nsobject<NSMenu> menu_; 43 }; 44 45 class HistoryMenuBridgeTest : public CocoaTest { 46 public: 47 48 virtual void SetUp() { 49 CocoaTest::SetUp(); 50 browser_test_helper_.profile()->CreateFaviconService(); 51 bridge_.reset(new MockBridge(browser_test_helper_.profile())); 52 } 53 54 // We are a friend of HistoryMenuBridge (and have access to 55 // protected methods), but none of the classes generated by TEST_F() 56 // are. Wraps common commands. 57 void ClearMenuSection(NSMenu* menu, 58 NSInteger tag) { 59 bridge_->ClearMenuSection(menu, tag); 60 } 61 62 void AddItemToBridgeMenu(HistoryMenuBridge::HistoryItem* item, 63 NSMenu* menu, 64 NSInteger tag, 65 NSInteger index) { 66 bridge_->AddItemToMenu(item, menu, tag, index); 67 } 68 69 NSMenuItem* AddItemToMenu(NSMenu* menu, 70 NSString* title, 71 SEL selector, 72 int tag) { 73 NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title action:NULL 74 keyEquivalent:@""] autorelease]; 75 [item setTag:tag]; 76 if (selector) { 77 [item setAction:selector]; 78 [item setTarget:bridge_->controller_.get()]; 79 } 80 [menu addItem:item]; 81 return item; 82 } 83 84 HistoryMenuBridge::HistoryItem* CreateItem(const string16& title) { 85 HistoryMenuBridge::HistoryItem* item = 86 new HistoryMenuBridge::HistoryItem(); 87 item->title = title; 88 item->url = GURL(title); 89 return item; 90 } 91 92 MockTRS::Tab CreateSessionTab(const GURL& url, const string16& title) { 93 MockTRS::Tab tab; 94 tab.current_navigation_index = 0; 95 tab.navigations.push_back( 96 TabNavigation(0, url, GURL(), title, std::string(), 97 PageTransition::LINK)); 98 return tab; 99 } 100 101 void GetFaviconForHistoryItem(HistoryMenuBridge::HistoryItem* item) { 102 bridge_->GetFaviconForHistoryItem(item); 103 } 104 105 void GotFaviconData(FaviconService::Handle handle, 106 history::FaviconData favicon) { 107 bridge_->GotFaviconData(handle, favicon); 108 } 109 110 CancelableRequestConsumerTSimple<HistoryMenuBridge::HistoryItem*>& 111 favicon_consumer() { 112 return bridge_->favicon_consumer_; 113 } 114 115 BrowserTestHelper browser_test_helper_; 116 scoped_ptr<MockBridge> bridge_; 117 }; 118 119 // Edge case test for clearing until the end of a menu. 120 TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuUntilEnd) { 121 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; 122 AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kMostVisitedTitle); 123 124 NSInteger tag = HistoryMenuBridge::kMostVisited; 125 AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag); 126 AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag); 127 AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag); 128 AddItemToMenu(menu, @"delta", @selector(openHistoryMenuItem:), tag); 129 130 ClearMenuSection(menu, HistoryMenuBridge::kMostVisited); 131 132 EXPECT_EQ(1, [menu numberOfItems]); 133 EXPECT_NSEQ(@"HEADER", 134 [[menu itemWithTag:HistoryMenuBridge::kMostVisitedTitle] title]); 135 } 136 137 // Skip menu items that are not hooked up to |-openHistoryMenuItem:|. 138 TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuSkipping) { 139 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; 140 AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kMostVisitedTitle); 141 142 NSInteger tag = HistoryMenuBridge::kMostVisited; 143 AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag); 144 AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag); 145 AddItemToMenu(menu, @"TITLE", NULL, HistoryMenuBridge::kRecentlyClosedTitle); 146 AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag); 147 148 ClearMenuSection(menu, tag); 149 150 EXPECT_EQ(2, [menu numberOfItems]); 151 EXPECT_NSEQ(@"HEADER", 152 [[menu itemWithTag:HistoryMenuBridge::kMostVisitedTitle] title]); 153 EXPECT_NSEQ(@"TITLE", 154 [[menu itemAtIndex:1] title]); 155 } 156 157 // Edge case test for clearing an empty menu. 158 TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuEmpty) { 159 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; 160 AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kMostVisited); 161 162 ClearMenuSection(menu, HistoryMenuBridge::kMostVisited); 163 164 EXPECT_EQ(1, [menu numberOfItems]); 165 EXPECT_NSEQ(@"HEADER", 166 [[menu itemWithTag:HistoryMenuBridge::kMostVisited] title]); 167 } 168 169 // Test that AddItemToMenu() properly adds HistoryItem objects as menus. 170 TEST_F(HistoryMenuBridgeTest, AddItemToMenu) { 171 NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; 172 173 const string16 short_url = ASCIIToUTF16("http://foo/"); 174 const string16 long_url = ASCIIToUTF16("http://super-duper-long-url--." 175 "that.cannot.possibly.fit.even-in-80-columns" 176 "or.be.reasonably-displayed-in-a-menu" 177 "without.looking-ridiculous.com/"); // 140 chars total 178 179 // HistoryItems are owned by the HistoryMenuBridge when AddItemToBridgeMenu() 180 // is called, which places them into the |menu_item_map_|, which owns them. 181 HistoryMenuBridge::HistoryItem* item1 = CreateItem(short_url); 182 AddItemToBridgeMenu(item1, menu, 100, 0); 183 184 HistoryMenuBridge::HistoryItem* item2 = CreateItem(long_url); 185 AddItemToBridgeMenu(item2, menu, 101, 1); 186 187 EXPECT_EQ(2, [menu numberOfItems]); 188 189 EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:0] action]); 190 EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:1] action]); 191 192 EXPECT_EQ(100, [[menu itemAtIndex:0] tag]); 193 EXPECT_EQ(101, [[menu itemAtIndex:1] tag]); 194 195 // Make sure a short title looks fine 196 NSString* s = [[menu itemAtIndex:0] title]; 197 EXPECT_EQ(base::SysNSStringToUTF16(s), short_url); 198 199 // Make sure a super-long title gets trimmed 200 s = [[menu itemAtIndex:0] title]; 201 EXPECT_TRUE([s length] < long_url.length()); 202 203 // Confirm tooltips and confirm they are not trimmed (like the item 204 // name might be). Add tolerance for URL fixer-upping; 205 // e.g. http://foo becomes http://foo/) 206 EXPECT_GE([[[menu itemAtIndex:0] toolTip] length], (2*short_url.length()-5)); 207 EXPECT_GE([[[menu itemAtIndex:1] toolTip] length], (2*long_url.length()-5)); 208 } 209 210 // Test that the menu is created for a set of simple tabs. 211 TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabs) { 212 scoped_refptr<MockTRS> trs(new MockTRS(browser_test_helper_.profile())); 213 MockTRS::Entries entries; 214 215 MockTRS::Tab tab1 = CreateSessionTab(GURL("http://google.com"), 216 ASCIIToUTF16("Google")); 217 tab1.id = 24; 218 entries.push_back(&tab1); 219 220 MockTRS::Tab tab2 = CreateSessionTab(GURL("http://apple.com"), 221 ASCIIToUTF16("Apple")); 222 tab2.id = 42; 223 entries.push_back(&tab2); 224 225 using ::testing::ReturnRef; 226 EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries)); 227 228 bridge_->TabRestoreServiceChanged(trs.get()); 229 230 NSMenu* menu = bridge_->HistoryMenu(); 231 ASSERT_EQ(2U, [[menu itemArray] count]); 232 233 NSMenuItem* item1 = [menu itemAtIndex:0]; 234 MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1); 235 EXPECT_TRUE(hist1); 236 EXPECT_EQ(24, hist1->session_id); 237 EXPECT_NSEQ(@"Google", [item1 title]); 238 239 NSMenuItem* item2 = [menu itemAtIndex:1]; 240 MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2); 241 EXPECT_TRUE(hist2); 242 EXPECT_EQ(42, hist2->session_id); 243 EXPECT_NSEQ(@"Apple", [item2 title]); 244 } 245 246 // Test that the menu is created for a mix of windows and tabs. 247 TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabsAndWindows) { 248 scoped_refptr<MockTRS> trs(new MockTRS(browser_test_helper_.profile())); 249 MockTRS::Entries entries; 250 251 MockTRS::Tab tab1 = CreateSessionTab(GURL("http://google.com"), 252 ASCIIToUTF16("Google")); 253 tab1.id = 24; 254 entries.push_back(&tab1); 255 256 MockTRS::Window win1; 257 win1.id = 30; 258 win1.tabs.push_back( 259 CreateSessionTab(GURL("http://foo.com"), ASCIIToUTF16("foo"))); 260 win1.tabs[0].id = 31; 261 win1.tabs.push_back( 262 CreateSessionTab(GURL("http://bar.com"), ASCIIToUTF16("bar"))); 263 win1.tabs[1].id = 32; 264 entries.push_back(&win1); 265 266 MockTRS::Tab tab2 = CreateSessionTab(GURL("http://apple.com"), 267 ASCIIToUTF16("Apple")); 268 tab2.id = 42; 269 entries.push_back(&tab2); 270 271 MockTRS::Window win2; 272 win2.id = 50; 273 win2.tabs.push_back( 274 CreateSessionTab(GURL("http://magic.com"), ASCIIToUTF16("magic"))); 275 win2.tabs[0].id = 51; 276 win2.tabs.push_back( 277 CreateSessionTab(GURL("http://goats.com"), ASCIIToUTF16("goats"))); 278 win2.tabs[1].id = 52; 279 win2.tabs.push_back( 280 CreateSessionTab(GURL("http://teleporter.com"), 281 ASCIIToUTF16("teleporter"))); 282 win2.tabs[1].id = 53; 283 entries.push_back(&win2); 284 285 using ::testing::ReturnRef; 286 EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries)); 287 288 bridge_->TabRestoreServiceChanged(trs.get()); 289 290 NSMenu* menu = bridge_->HistoryMenu(); 291 ASSERT_EQ(4U, [[menu itemArray] count]); 292 293 NSMenuItem* item1 = [menu itemAtIndex:0]; 294 MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1); 295 EXPECT_TRUE(hist1); 296 EXPECT_EQ(24, hist1->session_id); 297 EXPECT_NSEQ(@"Google", [item1 title]); 298 299 NSMenuItem* item2 = [menu itemAtIndex:1]; 300 MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2); 301 EXPECT_TRUE(hist2); 302 EXPECT_EQ(30, hist2->session_id); 303 EXPECT_EQ(2U, hist2->tabs.size()); 304 // Do not test menu item title because it is localized. 305 NSMenu* submenu1 = [item2 submenu]; 306 EXPECT_EQ(4U, [[submenu1 itemArray] count]); 307 // Do not test Restore All Tabs because it is localiced. 308 EXPECT_TRUE([[submenu1 itemAtIndex:1] isSeparatorItem]); 309 EXPECT_NSEQ(@"foo", [[submenu1 itemAtIndex:2] title]); 310 EXPECT_NSEQ(@"bar", [[submenu1 itemAtIndex:3] title]); 311 312 NSMenuItem* item3 = [menu itemAtIndex:2]; 313 MockBridge::HistoryItem* hist3 = bridge_->HistoryItemForMenuItem(item3); 314 EXPECT_TRUE(hist3); 315 EXPECT_EQ(42, hist3->session_id); 316 EXPECT_NSEQ(@"Apple", [item3 title]); 317 318 NSMenuItem* item4 = [menu itemAtIndex:3]; 319 MockBridge::HistoryItem* hist4 = bridge_->HistoryItemForMenuItem(item4); 320 EXPECT_TRUE(hist4); 321 EXPECT_EQ(50, hist4->session_id); 322 EXPECT_EQ(3U, hist4->tabs.size()); 323 // Do not test menu item title because it is localized. 324 NSMenu* submenu2 = [item4 submenu]; 325 EXPECT_EQ(5U, [[submenu2 itemArray] count]); 326 // Do not test Restore All Tabs because it is localiced. 327 EXPECT_TRUE([[submenu2 itemAtIndex:1] isSeparatorItem]); 328 EXPECT_NSEQ(@"magic", [[submenu2 itemAtIndex:2] title]); 329 EXPECT_NSEQ(@"goats", [[submenu2 itemAtIndex:3] title]); 330 EXPECT_NSEQ(@"teleporter", [[submenu2 itemAtIndex:4] title]); 331 } 332 333 // Tests that we properly request an icon from the FaviconService. 334 TEST_F(HistoryMenuBridgeTest, GetFaviconForHistoryItem) { 335 // Create a fake item. 336 HistoryMenuBridge::HistoryItem item; 337 item.title = ASCIIToUTF16("Title"); 338 item.url = GURL("http://google.com"); 339 340 // Request the icon. 341 GetFaviconForHistoryItem(&item); 342 343 // Make sure that there is ClientData for the request. 344 std::vector<HistoryMenuBridge::HistoryItem*> data; 345 favicon_consumer().GetAllClientData(&data); 346 ASSERT_EQ(data.size(), 1U); 347 EXPECT_EQ(&item, data[0]); 348 349 // Make sure the item was modified properly. 350 EXPECT_TRUE(item.icon_requested); 351 EXPECT_GT(item.icon_handle, 0); 352 } 353 354 TEST_F(HistoryMenuBridgeTest, GotFaviconData) { 355 // Create a dummy bitmap. 356 SkBitmap bitmap; 357 bitmap.setConfig(SkBitmap::kARGB_8888_Config, 25, 25); 358 bitmap.allocPixels(); 359 bitmap.eraseRGB(255, 0, 0); 360 361 // Convert it to raw PNG bytes. We totally ignore color order here because 362 // we just want to test the roundtrip through the Bridge, not that we can 363 // make icons look pretty. 364 std::vector<unsigned char> raw; 365 gfx::PNGCodec::EncodeBGRASkBitmap(bitmap, true, &raw); 366 scoped_refptr<RefCountedBytes> bytes(); 367 368 // Set up the HistoryItem. 369 HistoryMenuBridge::HistoryItem item; 370 item.menu_item.reset([[NSMenuItem alloc] init]); 371 GetFaviconForHistoryItem(&item); 372 373 // Pretend to be called back. 374 history::FaviconData favicon; 375 favicon.known_icon = true; 376 favicon.image_data = new RefCountedBytes(raw); 377 favicon.expired = false; 378 favicon.icon_url = GURL(); 379 favicon.icon_type = history::FAVICON; 380 GotFaviconData(item.icon_handle, favicon); 381 382 // Make sure the callback works. 383 EXPECT_FALSE(item.icon_requested); 384 EXPECT_TRUE(item.icon.get()); 385 EXPECT_TRUE([item.menu_item image]); 386 } 387 388 } // namespace 389