| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/ui/cocoa/history_menu_bridge.h" |
| |
| #import <Cocoa/Cocoa.h> |
| |
| #include <initializer_list> |
| #include <memory> |
| #include <vector> |
| |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/ref_counted_memory.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/sys_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "chrome/app/chrome_command_ids.h" |
| #include "chrome/browser/sessions/chrome_tab_restore_service_client.h" |
| #include "chrome/browser/ui/cocoa/test/cocoa_profile_test.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/favicon_base/favicon_types.h" |
| #include "components/sessions/core/serialized_navigation_entry_test_helper.h" |
| #include "components/sessions/core/tab_restore_service_impl.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #import "testing/gtest_mac.h" |
| #include "third_party/skia/include/core/SkBitmap.h" |
| #include "ui/gfx/codec/png_codec.h" |
| |
| namespace { |
| |
| class MockTRS : public sessions::TabRestoreServiceImpl { |
| public: |
| MockTRS(Profile* profile) |
| : sessions::TabRestoreServiceImpl( |
| base::WrapUnique(new ChromeTabRestoreServiceClient(profile)), |
| profile->GetPrefs(), |
| nullptr) {} |
| MOCK_CONST_METHOD0(entries, const sessions::TabRestoreService::Entries&()); |
| }; |
| |
| class MockBridge : public HistoryMenuBridge { |
| public: |
| MockBridge(Profile* profile) |
| : HistoryMenuBridge(profile), |
| menu_([[NSMenu alloc] initWithTitle:@"History"]) {} |
| |
| NSMenu* HistoryMenu() override { return menu_.get(); } |
| |
| private: |
| base::scoped_nsobject<NSMenu> menu_; |
| }; |
| |
| class HistoryMenuBridgeTest : public CocoaProfileTest { |
| public: |
| void SetUp() override { |
| CocoaProfileTest::SetUp(); |
| ASSERT_TRUE(profile()->CreateHistoryService(/*delete_file=*/true, |
| /*no_db=*/false)); |
| profile()->CreateFaviconService(); |
| bridge_.reset(new MockBridge(profile())); |
| } |
| |
| // We are a friend of HistoryMenuBridge (and have access to |
| // protected methods), but none of the classes generated by TEST_F() |
| // are. Wraps common commands. |
| void ClearMenuSection(NSMenu* menu, |
| NSInteger tag) { |
| bridge_->ClearMenuSection(menu, tag); |
| } |
| |
| void AddItemToBridgeMenu(HistoryMenuBridge::HistoryItem* item, |
| NSMenu* menu, |
| NSInteger tag, |
| NSInteger index) { |
| bridge_->AddItemToMenu(item, menu, tag, index); |
| } |
| |
| NSMenuItem* AddItemToMenu(NSMenu* menu, |
| NSString* title, |
| SEL selector, |
| int tag) { |
| NSMenuItem* item = [[[NSMenuItem alloc] initWithTitle:title action:NULL |
| keyEquivalent:@""] autorelease]; |
| [item setTag:tag]; |
| if (selector) { |
| [item setAction:selector]; |
| [item setTarget:bridge_->controller_.get()]; |
| } |
| [menu addItem:item]; |
| return item; |
| } |
| |
| HistoryMenuBridge::HistoryItem* CreateItem(const base::string16& title) { |
| HistoryMenuBridge::HistoryItem* item = |
| new HistoryMenuBridge::HistoryItem(); |
| item->title = title; |
| item->url = GURL(title); |
| return item; |
| } |
| |
| MockTRS::Entries CreateSessionEntries( |
| std::initializer_list<MockTRS::Entry*> entries) { |
| MockTRS::Entries ret; |
| for (auto* entry : entries) |
| ret.emplace_back(entry); |
| return ret; |
| } |
| |
| MockTRS::Tab* CreateSessionTab(SessionID::id_type id, |
| const std::string& url, |
| const std::string& title) { |
| auto* tab = new MockTRS::Tab; |
| tab->id = SessionID::FromSerializedValue(id); |
| tab->current_navigation_index = 0; |
| tab->navigations.push_back( |
| sessions::SerializedNavigationEntryTestHelper::CreateNavigation(url, |
| title)); |
| return tab; |
| } |
| |
| MockTRS::Window* CreateSessionWindow( |
| SessionID::id_type id, |
| std::initializer_list<MockTRS::Tab*> tabs) { |
| auto* window = new MockTRS::Window; |
| window->id = SessionID::FromSerializedValue(id); |
| window->tabs.reserve(tabs.size()); |
| for (auto* tab : tabs) |
| window->tabs.emplace_back(std::move(tab)); |
| return window; |
| } |
| |
| void GetFaviconForHistoryItem(HistoryMenuBridge::HistoryItem* item) { |
| bridge_->GetFaviconForHistoryItem(item); |
| } |
| |
| void GotFaviconData(HistoryMenuBridge::HistoryItem* item, |
| const favicon_base::FaviconImageResult& image_result) { |
| bridge_->GotFaviconData(item, image_result); |
| } |
| |
| void CancelFaviconRequest(HistoryMenuBridge::HistoryItem* item) { |
| bridge_->CancelFaviconRequest(item); |
| } |
| |
| std::unique_ptr<MockBridge> bridge_; |
| }; |
| |
| // Edge case test for clearing until the end of a menu. |
| TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuUntilEnd) { |
| NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; |
| AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kVisitedTitle); |
| |
| NSInteger tag = HistoryMenuBridge::kVisited; |
| AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag); |
| AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag); |
| AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag); |
| AddItemToMenu(menu, @"delta", @selector(openHistoryMenuItem:), tag); |
| |
| ClearMenuSection(menu, HistoryMenuBridge::kVisited); |
| |
| EXPECT_EQ(1, [menu numberOfItems]); |
| EXPECT_NSEQ(@"HEADER", |
| [[menu itemWithTag:HistoryMenuBridge::kVisitedTitle] title]); |
| } |
| |
| // Skip menu items that are not hooked up to |-openHistoryMenuItem:|. |
| TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuSkipping) { |
| NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; |
| AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kVisitedTitle); |
| |
| NSInteger tag = HistoryMenuBridge::kVisited; |
| AddItemToMenu(menu, @"alpha", @selector(openHistoryMenuItem:), tag); |
| AddItemToMenu(menu, @"bravo", @selector(openHistoryMenuItem:), tag); |
| AddItemToMenu(menu, @"TITLE", NULL, HistoryMenuBridge::kRecentlyClosedTitle); |
| AddItemToMenu(menu, @"charlie", @selector(openHistoryMenuItem:), tag); |
| |
| ClearMenuSection(menu, tag); |
| |
| EXPECT_EQ(2, [menu numberOfItems]); |
| EXPECT_NSEQ(@"HEADER", |
| [[menu itemWithTag:HistoryMenuBridge::kVisitedTitle] title]); |
| EXPECT_NSEQ(@"TITLE", |
| [[menu itemAtIndex:1] title]); |
| } |
| |
| // Edge case test for clearing an empty menu. |
| TEST_F(HistoryMenuBridgeTest, ClearHistoryMenuEmpty) { |
| NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; |
| AddItemToMenu(menu, @"HEADER", NULL, HistoryMenuBridge::kVisited); |
| |
| ClearMenuSection(menu, HistoryMenuBridge::kVisited); |
| |
| EXPECT_EQ(1, [menu numberOfItems]); |
| EXPECT_NSEQ(@"HEADER", |
| [[menu itemWithTag:HistoryMenuBridge::kVisited] title]); |
| } |
| |
| // Test that AddItemToMenu() properly adds HistoryItem objects as menus. |
| TEST_F(HistoryMenuBridgeTest, AddItemToMenu) { |
| NSMenu* menu = [[[NSMenu alloc] initWithTitle:@"history foo"] autorelease]; |
| |
| const base::string16 short_url = base::ASCIIToUTF16("http://foo/"); |
| const base::string16 long_url = base::ASCIIToUTF16( |
| "http://super-duper-long-url--." |
| "that.cannot.possibly.fit.even-in-80-columns" |
| "or.be.reasonably-displayed-in-a-menu" |
| "without.looking-ridiculous.com/"); // 140 chars total |
| |
| // HistoryItems are owned by the HistoryMenuBridge when AddItemToBridgeMenu() |
| // is called, which places them into the |menu_item_map_|, which owns them. |
| HistoryMenuBridge::HistoryItem* item1 = CreateItem(short_url); |
| AddItemToBridgeMenu(item1, menu, 100, 0); |
| |
| HistoryMenuBridge::HistoryItem* item2 = CreateItem(long_url); |
| AddItemToBridgeMenu(item2, menu, 101, 1); |
| |
| EXPECT_EQ(2, [menu numberOfItems]); |
| |
| EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:0] action]); |
| EXPECT_EQ(@selector(openHistoryMenuItem:), [[menu itemAtIndex:1] action]); |
| |
| EXPECT_EQ(100, [[menu itemAtIndex:0] tag]); |
| EXPECT_EQ(101, [[menu itemAtIndex:1] tag]); |
| |
| // Make sure a short title looks fine |
| NSString* s = [[menu itemAtIndex:0] title]; |
| EXPECT_EQ(base::SysNSStringToUTF16(s), short_url); |
| |
| // Make sure a super-long title gets trimmed |
| s = [[menu itemAtIndex:0] title]; |
| EXPECT_TRUE([s length] < long_url.length()); |
| |
| // Confirm tooltips and confirm they are not trimmed (like the item |
| // name might be). Add tolerance for URL fixer-upping; |
| // e.g. http://foo becomes http://foo/) |
| EXPECT_GE([[[menu itemAtIndex:0] toolTip] length], (2*short_url.length()-5)); |
| EXPECT_GE([[[menu itemAtIndex:1] toolTip] length], (2*long_url.length()-5)); |
| } |
| |
| // Test that the menu is created for a set of simple tabs. |
| TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabs) { |
| std::unique_ptr<MockTRS> trs(new MockTRS(profile())); |
| auto entries{CreateSessionEntries({ |
| CreateSessionTab(24, "http://google.com", "Google"), |
| CreateSessionTab(42, "http://apple.com", "Apple"), |
| })}; |
| |
| using ::testing::ReturnRef; |
| EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries)); |
| |
| bridge_->TabRestoreServiceChanged(trs.get()); |
| |
| NSMenu* menu = bridge_->HistoryMenu(); |
| ASSERT_EQ(2U, [[menu itemArray] count]); |
| |
| NSMenuItem* item1 = [menu itemAtIndex:0]; |
| MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1); |
| EXPECT_TRUE(hist1); |
| EXPECT_EQ(24, hist1->session_id.id()); |
| EXPECT_NSEQ(@"Google", [item1 title]); |
| |
| NSMenuItem* item2 = [menu itemAtIndex:1]; |
| MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2); |
| EXPECT_TRUE(hist2); |
| EXPECT_EQ(42, hist2->session_id.id()); |
| EXPECT_NSEQ(@"Apple", [item2 title]); |
| } |
| |
| // Test that the menu is created for a mix of windows and tabs. |
| TEST_F(HistoryMenuBridgeTest, RecentlyClosedTabsAndWindows) { |
| std::unique_ptr<MockTRS> trs(new MockTRS(profile())); |
| auto entries{CreateSessionEntries({ |
| CreateSessionTab(24, "http://google.com", "Google"), |
| CreateSessionWindow(30, { |
| CreateSessionTab(31, "http://foo.com", "foo"), |
| CreateSessionTab(32, "http://bar.com", "bar"), |
| }), |
| CreateSessionTab(42, "http://apple.com", "Apple"), |
| CreateSessionWindow(50, { |
| CreateSessionTab(51, "http://magic.com", "magic"), |
| CreateSessionTab(52, "http://goats.com", "goats"), |
| CreateSessionTab(53, "http://teleporter.com", "teleporter"), |
| }), |
| })}; |
| |
| using ::testing::ReturnRef; |
| EXPECT_CALL(*trs.get(), entries()).WillOnce(ReturnRef(entries)); |
| |
| bridge_->TabRestoreServiceChanged(trs.get()); |
| |
| NSMenu* menu = bridge_->HistoryMenu(); |
| ASSERT_EQ(4U, [[menu itemArray] count]); |
| |
| NSMenuItem* item1 = [menu itemAtIndex:0]; |
| MockBridge::HistoryItem* hist1 = bridge_->HistoryItemForMenuItem(item1); |
| EXPECT_TRUE(hist1); |
| EXPECT_EQ(24, hist1->session_id.id()); |
| EXPECT_NSEQ(@"Google", [item1 title]); |
| |
| NSMenuItem* item2 = [menu itemAtIndex:1]; |
| MockBridge::HistoryItem* hist2 = bridge_->HistoryItemForMenuItem(item2); |
| EXPECT_TRUE(hist2); |
| EXPECT_EQ(30, hist2->session_id.id()); |
| EXPECT_EQ(2U, hist2->tabs.size()); |
| // Do not test menu item title because it is localized. |
| NSMenu* submenu1 = [item2 submenu]; |
| EXPECT_EQ(4U, [[submenu1 itemArray] count]); |
| // Do not test Restore All Tabs because it is localiced. |
| EXPECT_TRUE([[submenu1 itemAtIndex:1] isSeparatorItem]); |
| EXPECT_NSEQ(@"foo", [[submenu1 itemAtIndex:2] title]); |
| EXPECT_NSEQ(@"bar", [[submenu1 itemAtIndex:3] title]); |
| EXPECT_EQ(31, hist2->tabs[0]->session_id.id()); |
| EXPECT_EQ(32, hist2->tabs[1]->session_id.id()); |
| |
| NSMenuItem* item3 = [menu itemAtIndex:2]; |
| MockBridge::HistoryItem* hist3 = bridge_->HistoryItemForMenuItem(item3); |
| EXPECT_TRUE(hist3); |
| EXPECT_EQ(42, hist3->session_id.id()); |
| EXPECT_NSEQ(@"Apple", [item3 title]); |
| |
| NSMenuItem* item4 = [menu itemAtIndex:3]; |
| MockBridge::HistoryItem* hist4 = bridge_->HistoryItemForMenuItem(item4); |
| EXPECT_TRUE(hist4); |
| EXPECT_EQ(50, hist4->session_id.id()); |
| EXPECT_EQ(3U, hist4->tabs.size()); |
| // Do not test menu item title because it is localized. |
| NSMenu* submenu2 = [item4 submenu]; |
| EXPECT_EQ(5U, [[submenu2 itemArray] count]); |
| // Do not test Restore All Tabs because it is localiced. |
| EXPECT_TRUE([[submenu2 itemAtIndex:1] isSeparatorItem]); |
| EXPECT_NSEQ(@"magic", [[submenu2 itemAtIndex:2] title]); |
| EXPECT_NSEQ(@"goats", [[submenu2 itemAtIndex:3] title]); |
| EXPECT_NSEQ(@"teleporter", [[submenu2 itemAtIndex:4] title]); |
| EXPECT_EQ(51, hist4->tabs[0]->session_id.id()); |
| EXPECT_EQ(52, hist4->tabs[1]->session_id.id()); |
| EXPECT_EQ(53, hist4->tabs[2]->session_id.id()); |
| } |
| |
| // Tests that we properly request an icon from the FaviconService. |
| TEST_F(HistoryMenuBridgeTest, GetFaviconForHistoryItem) { |
| // Create a fake item. |
| HistoryMenuBridge::HistoryItem item; |
| item.title = base::ASCIIToUTF16("Title"); |
| item.url = GURL("http://google.com"); |
| |
| // Request the icon. |
| GetFaviconForHistoryItem(&item); |
| |
| // Make sure the item was modified properly. |
| EXPECT_TRUE(item.icon_requested); |
| EXPECT_NE(base::CancelableTaskTracker::kBadTaskId, item.icon_task_id); |
| |
| // Cancel the request. |
| CancelFaviconRequest(&item); |
| } |
| |
| TEST_F(HistoryMenuBridgeTest, GotFaviconData) { |
| // Create a dummy bitmap. |
| SkBitmap bitmap; |
| bitmap.allocN32Pixels(25, 25); |
| bitmap.eraseARGB(255, 255, 0, 0); |
| |
| // Set up the HistoryItem. |
| HistoryMenuBridge::HistoryItem item; |
| item.menu_item.reset([[NSMenuItem alloc] init]); |
| GetFaviconForHistoryItem(&item); |
| |
| // Cancel the request so there will be no race. |
| CancelFaviconRequest(&item); |
| |
| // Pretend to be called back. |
| favicon_base::FaviconImageResult image_result; |
| image_result.image = gfx::Image::CreateFrom1xBitmap(bitmap); |
| GotFaviconData(&item, image_result); |
| |
| // Make sure the callback works. |
| EXPECT_FALSE(item.icon_requested); |
| EXPECT_TRUE(item.icon.get()); |
| EXPECT_TRUE([item.menu_item image]); |
| } |
| |
| } // namespace |