1// Copyright 2014 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 "components/bookmarks/browser/bookmark_utils.h"
6
7#include <vector>
8
9#include "base/message_loop/message_loop.h"
10#include "base/strings/utf_string_conversions.h"
11#include "components/bookmarks/browser/base_bookmark_model_observer.h"
12#include "components/bookmarks/browser/bookmark_model.h"
13#include "components/bookmarks/browser/bookmark_node_data.h"
14#include "components/bookmarks/test/test_bookmark_client.h"
15#include "testing/gtest/include/gtest/gtest.h"
16#include "ui/base/clipboard/clipboard.h"
17#include "ui/base/clipboard/scoped_clipboard_writer.h"
18
19using base::ASCIIToUTF16;
20using std::string;
21
22namespace bookmarks {
23namespace {
24
25class BookmarkUtilsTest : public testing::Test,
26                          public BaseBookmarkModelObserver {
27 public:
28  BookmarkUtilsTest()
29      : grouped_changes_beginning_count_(0),
30        grouped_changes_ended_count_(0) {}
31  virtual ~BookmarkUtilsTest() {}
32
33// Copy and paste is not yet supported on iOS. http://crbug.com/228147
34#if !defined(OS_IOS)
35  virtual void TearDown() OVERRIDE {
36    ui::Clipboard::DestroyClipboardForCurrentThread();
37  }
38#endif  // !defined(OS_IOS)
39
40  // Certain user actions require multiple changes to the bookmark model,
41  // however these modifications need to be atomic for the undo framework. The
42  // BaseBookmarkModelObserver is used to inform the boundaries of the user
43  // action. For example, when multiple bookmarks are cut to the clipboard we
44  // expect one call each to GroupedBookmarkChangesBeginning/Ended.
45  void ExpectGroupedChangeCount(int expected_beginning_count,
46                                int expected_ended_count) {
47    // The undo framework is not used under Android.  Thus the group change
48    // events will not be fired and so should not be tested for Android.
49#if !defined(OS_ANDROID)
50    EXPECT_EQ(grouped_changes_beginning_count_, expected_beginning_count);
51    EXPECT_EQ(grouped_changes_ended_count_, expected_ended_count);
52#endif
53  }
54
55 private:
56  // BaseBookmarkModelObserver:
57  virtual void BookmarkModelChanged() OVERRIDE {}
58
59  virtual void GroupedBookmarkChangesBeginning(BookmarkModel* model) OVERRIDE {
60    ++grouped_changes_beginning_count_;
61  }
62
63  virtual void GroupedBookmarkChangesEnded(BookmarkModel* model) OVERRIDE {
64    ++grouped_changes_ended_count_;
65  }
66
67  int grouped_changes_beginning_count_;
68  int grouped_changes_ended_count_;
69
70  // Clipboard requires a message loop.
71  base::MessageLoopForUI loop_;
72
73  DISALLOW_COPY_AND_ASSIGN(BookmarkUtilsTest);
74};
75
76TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesWordPhraseQuery) {
77  TestBookmarkClient client;
78  scoped_ptr<BookmarkModel> model(client.CreateModel());
79  const BookmarkNode* node1 = model->AddURL(model->other_node(),
80                                            0,
81                                            ASCIIToUTF16("foo bar"),
82                                            GURL("http://www.google.com"));
83  const BookmarkNode* node2 = model->AddURL(model->other_node(),
84                                            0,
85                                            ASCIIToUTF16("baz buz"),
86                                            GURL("http://www.cnn.com"));
87  const BookmarkNode* folder1 =
88      model->AddFolder(model->other_node(), 0, ASCIIToUTF16("foo"));
89  std::vector<const BookmarkNode*> nodes;
90  QueryFields query;
91  query.word_phrase_query.reset(new base::string16);
92  // No nodes are returned for empty string.
93  *query.word_phrase_query = ASCIIToUTF16("");
94  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
95  EXPECT_TRUE(nodes.empty());
96  nodes.clear();
97
98  // No nodes are returned for space-only string.
99  *query.word_phrase_query = ASCIIToUTF16("   ");
100  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
101  EXPECT_TRUE(nodes.empty());
102  nodes.clear();
103
104  // Node "foo bar" and folder "foo" are returned in search results.
105  *query.word_phrase_query = ASCIIToUTF16("foo");
106  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
107  ASSERT_EQ(2U, nodes.size());
108  EXPECT_TRUE(nodes[0] == folder1);
109  EXPECT_TRUE(nodes[1] == node1);
110  nodes.clear();
111
112  // Ensure url matches return in search results.
113  *query.word_phrase_query = ASCIIToUTF16("cnn");
114  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
115  ASSERT_EQ(1U, nodes.size());
116  EXPECT_TRUE(nodes[0] == node2);
117  nodes.clear();
118
119  // Ensure folder "foo" is not returned in more specific search.
120  *query.word_phrase_query = ASCIIToUTF16("foo bar");
121  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
122  ASSERT_EQ(1U, nodes.size());
123  EXPECT_TRUE(nodes[0] == node1);
124  nodes.clear();
125
126  // Bookmark Bar and Other Bookmarks are not returned in search results.
127  *query.word_phrase_query = ASCIIToUTF16("Bookmark");
128  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
129  ASSERT_EQ(0U, nodes.size());
130  nodes.clear();
131}
132
133// Check exact matching against a URL query.
134TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesUrl) {
135  TestBookmarkClient client;
136  scoped_ptr<BookmarkModel> model(client.CreateModel());
137  const BookmarkNode* node1 = model->AddURL(model->other_node(),
138                                            0,
139                                            ASCIIToUTF16("Google"),
140                                            GURL("https://www.google.com/"));
141  model->AddURL(model->other_node(),
142                0,
143                ASCIIToUTF16("Google Calendar"),
144                GURL("https://www.google.com/calendar"));
145
146  model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder"));
147
148  std::vector<const BookmarkNode*> nodes;
149  QueryFields query;
150  query.url.reset(new base::string16);
151  *query.url = ASCIIToUTF16("https://www.google.com/");
152  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
153  ASSERT_EQ(1U, nodes.size());
154  EXPECT_TRUE(nodes[0] == node1);
155  nodes.clear();
156
157  *query.url = ASCIIToUTF16("calendar");
158  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
159  ASSERT_EQ(0U, nodes.size());
160  nodes.clear();
161
162  // Empty URL should not match folders.
163  *query.url = ASCIIToUTF16("");
164  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
165  ASSERT_EQ(0U, nodes.size());
166  nodes.clear();
167}
168
169// Check exact matching against a title query.
170TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesTitle) {
171  TestBookmarkClient client;
172  scoped_ptr<BookmarkModel> model(client.CreateModel());
173  const BookmarkNode* node1 = model->AddURL(model->other_node(),
174                                            0,
175                                            ASCIIToUTF16("Google"),
176                                            GURL("https://www.google.com/"));
177  model->AddURL(model->other_node(),
178                0,
179                ASCIIToUTF16("Google Calendar"),
180                GURL("https://www.google.com/calendar"));
181
182  const BookmarkNode* folder1 =
183      model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder"));
184
185  std::vector<const BookmarkNode*> nodes;
186  QueryFields query;
187  query.title.reset(new base::string16);
188  *query.title = ASCIIToUTF16("Google");
189  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
190  ASSERT_EQ(1U, nodes.size());
191  EXPECT_TRUE(nodes[0] == node1);
192  nodes.clear();
193
194  *query.title = ASCIIToUTF16("Calendar");
195  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
196  ASSERT_EQ(0U, nodes.size());
197  nodes.clear();
198
199  // Title should match folders.
200  *query.title = ASCIIToUTF16("Folder");
201  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
202  ASSERT_EQ(1U, nodes.size());
203  EXPECT_TRUE(nodes[0] == folder1);
204  nodes.clear();
205}
206
207// Check matching against a query with multiple predicates.
208TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesConjunction) {
209  TestBookmarkClient client;
210  scoped_ptr<BookmarkModel> model(client.CreateModel());
211  const BookmarkNode* node1 = model->AddURL(model->other_node(),
212                                            0,
213                                            ASCIIToUTF16("Google"),
214                                            GURL("https://www.google.com/"));
215  model->AddURL(model->other_node(),
216                0,
217                ASCIIToUTF16("Google Calendar"),
218                GURL("https://www.google.com/calendar"));
219
220  model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder"));
221
222  std::vector<const BookmarkNode*> nodes;
223  QueryFields query;
224
225  // Test all fields matching.
226  query.word_phrase_query.reset(new base::string16(ASCIIToUTF16("www")));
227  query.url.reset(new base::string16(ASCIIToUTF16("https://www.google.com/")));
228  query.title.reset(new base::string16(ASCIIToUTF16("Google")));
229  GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
230  ASSERT_EQ(1U, nodes.size());
231  EXPECT_TRUE(nodes[0] == node1);
232  nodes.clear();
233
234  scoped_ptr<base::string16>* fields[] = {
235    &query.word_phrase_query, &query.url, &query.title };
236
237  // Test two fields matching.
238  for (size_t i = 0; i < arraysize(fields); i++) {
239    scoped_ptr<base::string16> original_value(fields[i]->release());
240    GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
241    ASSERT_EQ(1U, nodes.size());
242    EXPECT_TRUE(nodes[0] == node1);
243    nodes.clear();
244    fields[i]->reset(original_value.release());
245  }
246
247  // Test two fields matching with one non-matching field.
248  for (size_t i = 0; i < arraysize(fields); i++) {
249    scoped_ptr<base::string16> original_value(fields[i]->release());
250    fields[i]->reset(new base::string16(ASCIIToUTF16("fjdkslafjkldsa")));
251    GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes);
252    ASSERT_EQ(0U, nodes.size());
253    nodes.clear();
254    fields[i]->reset(original_value.release());
255  }
256}
257
258// Copy and paste is not yet supported on iOS. http://crbug.com/228147
259#if !defined(OS_IOS)
260TEST_F(BookmarkUtilsTest, PasteBookmarkFromURL) {
261  TestBookmarkClient client;
262  scoped_ptr<BookmarkModel> model(client.CreateModel());
263  const base::string16 url_text = ASCIIToUTF16("http://www.google.com/");
264  const BookmarkNode* new_folder = model->AddFolder(
265      model->bookmark_bar_node(), 0, ASCIIToUTF16("New_Folder"));
266
267  // Write blank text to clipboard.
268  {
269    ui::ScopedClipboardWriter clipboard_writer(ui::CLIPBOARD_TYPE_COPY_PASTE);
270    clipboard_writer.WriteText(base::string16());
271  }
272  // Now we shouldn't be able to paste from the clipboard.
273  EXPECT_FALSE(CanPasteFromClipboard(model.get(), new_folder));
274
275  // Write some valid url to the clipboard.
276  {
277    ui::ScopedClipboardWriter clipboard_writer(ui::CLIPBOARD_TYPE_COPY_PASTE);
278    clipboard_writer.WriteText(url_text);
279  }
280  // Now we should be able to paste from the clipboard.
281  EXPECT_TRUE(CanPasteFromClipboard(model.get(), new_folder));
282
283  PasteFromClipboard(model.get(), new_folder, 0);
284  ASSERT_EQ(1, new_folder->child_count());
285
286  // Url for added node should be same as url_text.
287  EXPECT_EQ(url_text, ASCIIToUTF16(new_folder->GetChild(0)->url().spec()));
288}
289
290TEST_F(BookmarkUtilsTest, CopyPaste) {
291  TestBookmarkClient client;
292  scoped_ptr<BookmarkModel> model(client.CreateModel());
293  const BookmarkNode* node = model->AddURL(model->other_node(),
294                                           0,
295                                           ASCIIToUTF16("foo bar"),
296                                           GURL("http://www.google.com"));
297
298  // Copy a node to the clipboard.
299  std::vector<const BookmarkNode*> nodes;
300  nodes.push_back(node);
301  CopyToClipboard(model.get(), nodes, false);
302
303  // And make sure we can paste a bookmark from the clipboard.
304  EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node()));
305
306  // Write some text to the clipboard.
307  {
308    ui::ScopedClipboardWriter clipboard_writer(
309        ui::CLIPBOARD_TYPE_COPY_PASTE);
310    clipboard_writer.WriteText(ASCIIToUTF16("foo"));
311  }
312
313  // Now we shouldn't be able to paste from the clipboard.
314  EXPECT_FALSE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node()));
315}
316
317TEST_F(BookmarkUtilsTest, CopyPasteMetaInfo) {
318  TestBookmarkClient client;
319  scoped_ptr<BookmarkModel> model(client.CreateModel());
320  const BookmarkNode* node = model->AddURL(model->other_node(),
321                                           0,
322                                           ASCIIToUTF16("foo bar"),
323                                           GURL("http://www.google.com"));
324  model->SetNodeMetaInfo(node, "somekey", "somevalue");
325  model->SetNodeMetaInfo(node, "someotherkey", "someothervalue");
326
327  // Copy a node to the clipboard.
328  std::vector<const BookmarkNode*> nodes;
329  nodes.push_back(node);
330  CopyToClipboard(model.get(), nodes, false);
331
332  // Paste node to a different folder.
333  const BookmarkNode* folder =
334      model->AddFolder(model->bookmark_bar_node(), 0, ASCIIToUTF16("Folder"));
335  EXPECT_EQ(0, folder->child_count());
336
337  // And make sure we can paste a bookmark from the clipboard.
338  EXPECT_TRUE(CanPasteFromClipboard(model.get(), folder));
339
340  PasteFromClipboard(model.get(), folder, 0);
341  ASSERT_EQ(1, folder->child_count());
342
343  // Verify that the pasted node contains the same meta info.
344  const BookmarkNode* pasted = folder->GetChild(0);
345  ASSERT_TRUE(pasted->GetMetaInfoMap());
346  EXPECT_EQ(2u, pasted->GetMetaInfoMap()->size());
347  std::string value;
348  EXPECT_TRUE(pasted->GetMetaInfo("somekey", &value));
349  EXPECT_EQ("somevalue", value);
350  EXPECT_TRUE(pasted->GetMetaInfo("someotherkey", &value));
351  EXPECT_EQ("someothervalue", value);
352}
353
354#if defined(OS_LINUX) || defined(OS_MACOSX)
355// http://crbug.com/396472
356#define MAYBE_CutToClipboard DISABLED_CutToClipboard
357#else
358#define MAYBE_CutToClipboard CutToClipboard
359#endif
360TEST_F(BookmarkUtilsTest, MAYBE_CutToClipboard) {
361  TestBookmarkClient client;
362  scoped_ptr<BookmarkModel> model(client.CreateModel());
363  model->AddObserver(this);
364
365  base::string16 title(ASCIIToUTF16("foo"));
366  GURL url("http://foo.com");
367  const BookmarkNode* n1 = model->AddURL(model->other_node(), 0, title, url);
368  const BookmarkNode* n2 = model->AddURL(model->other_node(), 1, title, url);
369
370  // Cut the nodes to the clipboard.
371  std::vector<const BookmarkNode*> nodes;
372  nodes.push_back(n1);
373  nodes.push_back(n2);
374  CopyToClipboard(model.get(), nodes, true);
375
376  // Make sure the nodes were removed.
377  EXPECT_EQ(0, model->other_node()->child_count());
378
379  // Make sure observers were notified the set of changes should be grouped.
380  ExpectGroupedChangeCount(1, 1);
381
382  // And make sure we can paste from the clipboard.
383  EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->other_node()));
384}
385
386TEST_F(BookmarkUtilsTest, PasteNonEditableNodes) {
387  TestBookmarkClient client;
388  // Load a model with an extra node that is not editable.
389  BookmarkPermanentNode* extra_node = new BookmarkPermanentNode(100);
390  BookmarkPermanentNodeList extra_nodes;
391  extra_nodes.push_back(extra_node);
392  client.SetExtraNodesToLoad(extra_nodes.Pass());
393
394  scoped_ptr<BookmarkModel> model(client.CreateModel());
395  const BookmarkNode* node = model->AddURL(model->other_node(),
396                                           0,
397                                           ASCIIToUTF16("foo bar"),
398                                           GURL("http://www.google.com"));
399
400  // Copy a node to the clipboard.
401  std::vector<const BookmarkNode*> nodes;
402  nodes.push_back(node);
403  CopyToClipboard(model.get(), nodes, false);
404
405  // And make sure we can paste a bookmark from the clipboard.
406  EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node()));
407
408  // But it can't be pasted into a non-editable folder.
409  BookmarkClient* upcast = &client;
410  EXPECT_FALSE(upcast->CanBeEditedByUser(extra_node));
411  EXPECT_FALSE(CanPasteFromClipboard(model.get(), extra_node));
412}
413#endif  // !defined(OS_IOS)
414
415TEST_F(BookmarkUtilsTest, GetParentForNewNodes) {
416  TestBookmarkClient client;
417  scoped_ptr<BookmarkModel> model(client.CreateModel());
418  // This tests the case where selection contains one item and that item is a
419  // folder.
420  std::vector<const BookmarkNode*> nodes;
421  nodes.push_back(model->bookmark_bar_node());
422  int index = -1;
423  const BookmarkNode* real_parent =
424      GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index);
425  EXPECT_EQ(real_parent, model->bookmark_bar_node());
426  EXPECT_EQ(0, index);
427
428  nodes.clear();
429
430  // This tests the case where selection contains one item and that item is an
431  // url.
432  const BookmarkNode* page1 = model->AddURL(model->bookmark_bar_node(),
433                                            0,
434                                            ASCIIToUTF16("Google"),
435                                            GURL("http://google.com"));
436  nodes.push_back(page1);
437  real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index);
438  EXPECT_EQ(real_parent, model->bookmark_bar_node());
439  EXPECT_EQ(1, index);
440
441  // This tests the case where selection has more than one item.
442  const BookmarkNode* folder1 =
443      model->AddFolder(model->bookmark_bar_node(), 1, ASCIIToUTF16("Folder 1"));
444  nodes.push_back(folder1);
445  real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index);
446  EXPECT_EQ(real_parent, model->bookmark_bar_node());
447  EXPECT_EQ(2, index);
448
449  // This tests the case where selection doesn't contain any items.
450  nodes.clear();
451  real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index);
452  EXPECT_EQ(real_parent, model->bookmark_bar_node());
453  EXPECT_EQ(2, index);
454}
455
456// Verifies that meta info is copied when nodes are cloned.
457TEST_F(BookmarkUtilsTest, CloneMetaInfo) {
458  TestBookmarkClient client;
459  scoped_ptr<BookmarkModel> model(client.CreateModel());
460  // Add a node containing meta info.
461  const BookmarkNode* node = model->AddURL(model->other_node(),
462                                           0,
463                                           ASCIIToUTF16("foo bar"),
464                                           GURL("http://www.google.com"));
465  model->SetNodeMetaInfo(node, "somekey", "somevalue");
466  model->SetNodeMetaInfo(node, "someotherkey", "someothervalue");
467
468  // Clone node to a different folder.
469  const BookmarkNode* folder =
470      model->AddFolder(model->bookmark_bar_node(), 0, ASCIIToUTF16("Folder"));
471  std::vector<BookmarkNodeData::Element> elements;
472  BookmarkNodeData::Element node_data(node);
473  elements.push_back(node_data);
474  EXPECT_EQ(0, folder->child_count());
475  CloneBookmarkNode(model.get(), elements, folder, 0, false);
476  ASSERT_EQ(1, folder->child_count());
477
478  // Verify that the cloned node contains the same meta info.
479  const BookmarkNode* clone = folder->GetChild(0);
480  ASSERT_TRUE(clone->GetMetaInfoMap());
481  EXPECT_EQ(2u, clone->GetMetaInfoMap()->size());
482  std::string value;
483  EXPECT_TRUE(clone->GetMetaInfo("somekey", &value));
484  EXPECT_EQ("somevalue", value);
485  EXPECT_TRUE(clone->GetMetaInfo("someotherkey", &value));
486  EXPECT_EQ("someothervalue", value);
487}
488
489TEST_F(BookmarkUtilsTest, RemoveAllBookmarks) {
490  TestBookmarkClient client;
491  // Load a model with an extra node that is not editable.
492  BookmarkPermanentNode* extra_node = new BookmarkPermanentNode(100);
493  BookmarkPermanentNodeList extra_nodes;
494  extra_nodes.push_back(extra_node);
495  client.SetExtraNodesToLoad(extra_nodes.Pass());
496
497  scoped_ptr<BookmarkModel> model(client.CreateModel());
498  EXPECT_TRUE(model->bookmark_bar_node()->empty());
499  EXPECT_TRUE(model->other_node()->empty());
500  EXPECT_TRUE(model->mobile_node()->empty());
501  EXPECT_TRUE(extra_node->empty());
502
503  const base::string16 title = base::ASCIIToUTF16("Title");
504  const GURL url("http://google.com");
505  model->AddURL(model->bookmark_bar_node(), 0, title, url);
506  model->AddURL(model->other_node(), 0, title, url);
507  model->AddURL(model->mobile_node(), 0, title, url);
508  model->AddURL(extra_node, 0, title, url);
509
510  std::vector<const BookmarkNode*> nodes;
511  model->GetNodesByURL(url, &nodes);
512  ASSERT_EQ(4u, nodes.size());
513
514  RemoveAllBookmarks(model.get(), url);
515
516  nodes.clear();
517  model->GetNodesByURL(url, &nodes);
518  ASSERT_EQ(1u, nodes.size());
519  EXPECT_TRUE(model->bookmark_bar_node()->empty());
520  EXPECT_TRUE(model->other_node()->empty());
521  EXPECT_TRUE(model->mobile_node()->empty());
522  EXPECT_EQ(1, extra_node->child_count());
523}
524
525}  // namespace
526}  // namespace bookmarks
527