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 19 using base::ASCIIToUTF16; 20 using std::string; 21 22 namespace bookmarks { 23 namespace { 24 25 class 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 76 TEST_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. 134 TEST_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. 170 TEST_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. 208 TEST_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) 260 TEST_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 290 TEST_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 317 TEST_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 360 TEST_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 386 TEST_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 415 TEST_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. 457 TEST_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 489 TEST_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