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