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