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