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