// Copyright 2014 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 "components/bookmarks/browser/bookmark_utils.h" #include #include "base/message_loop/message_loop.h" #include "base/strings/utf_string_conversions.h" #include "components/bookmarks/browser/base_bookmark_model_observer.h" #include "components/bookmarks/browser/bookmark_model.h" #include "components/bookmarks/browser/bookmark_node_data.h" #include "components/bookmarks/test/test_bookmark_client.h" #include "testing/gtest/include/gtest/gtest.h" #include "ui/base/clipboard/clipboard.h" #include "ui/base/clipboard/scoped_clipboard_writer.h" using base::ASCIIToUTF16; using std::string; namespace bookmarks { namespace { class BookmarkUtilsTest : public testing::Test, public BaseBookmarkModelObserver { public: BookmarkUtilsTest() : grouped_changes_beginning_count_(0), grouped_changes_ended_count_(0) {} ~BookmarkUtilsTest() override {} // Copy and paste is not yet supported on iOS. http://crbug.com/228147 #if !defined(OS_IOS) void TearDown() override { ui::Clipboard::DestroyClipboardForCurrentThread(); } #endif // !defined(OS_IOS) // Certain user actions require multiple changes to the bookmark model, // however these modifications need to be atomic for the undo framework. The // BaseBookmarkModelObserver is used to inform the boundaries of the user // action. For example, when multiple bookmarks are cut to the clipboard we // expect one call each to GroupedBookmarkChangesBeginning/Ended. void ExpectGroupedChangeCount(int expected_beginning_count, int expected_ended_count) { // The undo framework is not used under Android. Thus the group change // events will not be fired and so should not be tested for Android. #if !defined(OS_ANDROID) EXPECT_EQ(grouped_changes_beginning_count_, expected_beginning_count); EXPECT_EQ(grouped_changes_ended_count_, expected_ended_count); #endif } private: // BaseBookmarkModelObserver: void BookmarkModelChanged() override {} void GroupedBookmarkChangesBeginning(BookmarkModel* model) override { ++grouped_changes_beginning_count_; } void GroupedBookmarkChangesEnded(BookmarkModel* model) override { ++grouped_changes_ended_count_; } int grouped_changes_beginning_count_; int grouped_changes_ended_count_; // Clipboard requires a message loop. base::MessageLoopForUI loop_; DISALLOW_COPY_AND_ASSIGN(BookmarkUtilsTest); }; TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesWordPhraseQuery) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const BookmarkNode* node1 = model->AddURL(model->other_node(), 0, ASCIIToUTF16("foo bar"), GURL("http://www.google.com")); const BookmarkNode* node2 = model->AddURL(model->other_node(), 0, ASCIIToUTF16("baz buz"), GURL("http://www.cnn.com")); const BookmarkNode* folder1 = model->AddFolder(model->other_node(), 0, ASCIIToUTF16("foo")); std::vector nodes; QueryFields query; query.word_phrase_query.reset(new base::string16); // No nodes are returned for empty string. *query.word_phrase_query = ASCIIToUTF16(""); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); EXPECT_TRUE(nodes.empty()); nodes.clear(); // No nodes are returned for space-only string. *query.word_phrase_query = ASCIIToUTF16(" "); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); EXPECT_TRUE(nodes.empty()); nodes.clear(); // Node "foo bar" and folder "foo" are returned in search results. *query.word_phrase_query = ASCIIToUTF16("foo"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(2U, nodes.size()); EXPECT_TRUE(nodes[0] == folder1); EXPECT_TRUE(nodes[1] == node1); nodes.clear(); // Ensure url matches return in search results. *query.word_phrase_query = ASCIIToUTF16("cnn"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(1U, nodes.size()); EXPECT_TRUE(nodes[0] == node2); nodes.clear(); // Ensure folder "foo" is not returned in more specific search. *query.word_phrase_query = ASCIIToUTF16("foo bar"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(1U, nodes.size()); EXPECT_TRUE(nodes[0] == node1); nodes.clear(); // Bookmark Bar and Other Bookmarks are not returned in search results. *query.word_phrase_query = ASCIIToUTF16("Bookmark"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(0U, nodes.size()); nodes.clear(); } // Check exact matching against a URL query. TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesUrl) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const BookmarkNode* node1 = model->AddURL(model->other_node(), 0, ASCIIToUTF16("Google"), GURL("https://www.google.com/")); model->AddURL(model->other_node(), 0, ASCIIToUTF16("Google Calendar"), GURL("https://www.google.com/calendar")); model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder")); std::vector nodes; QueryFields query; query.url.reset(new base::string16); *query.url = ASCIIToUTF16("https://www.google.com/"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(1U, nodes.size()); EXPECT_TRUE(nodes[0] == node1); nodes.clear(); *query.url = ASCIIToUTF16("calendar"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(0U, nodes.size()); nodes.clear(); // Empty URL should not match folders. *query.url = ASCIIToUTF16(""); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(0U, nodes.size()); nodes.clear(); } // Check exact matching against a title query. TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesTitle) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const BookmarkNode* node1 = model->AddURL(model->other_node(), 0, ASCIIToUTF16("Google"), GURL("https://www.google.com/")); model->AddURL(model->other_node(), 0, ASCIIToUTF16("Google Calendar"), GURL("https://www.google.com/calendar")); const BookmarkNode* folder1 = model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder")); std::vector nodes; QueryFields query; query.title.reset(new base::string16); *query.title = ASCIIToUTF16("Google"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(1U, nodes.size()); EXPECT_TRUE(nodes[0] == node1); nodes.clear(); *query.title = ASCIIToUTF16("Calendar"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(0U, nodes.size()); nodes.clear(); // Title should match folders. *query.title = ASCIIToUTF16("Folder"); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(1U, nodes.size()); EXPECT_TRUE(nodes[0] == folder1); nodes.clear(); } // Check matching against a query with multiple predicates. TEST_F(BookmarkUtilsTest, GetBookmarksMatchingPropertiesConjunction) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const BookmarkNode* node1 = model->AddURL(model->other_node(), 0, ASCIIToUTF16("Google"), GURL("https://www.google.com/")); model->AddURL(model->other_node(), 0, ASCIIToUTF16("Google Calendar"), GURL("https://www.google.com/calendar")); model->AddFolder(model->other_node(), 0, ASCIIToUTF16("Folder")); std::vector nodes; QueryFields query; // Test all fields matching. query.word_phrase_query.reset(new base::string16(ASCIIToUTF16("www"))); query.url.reset(new base::string16(ASCIIToUTF16("https://www.google.com/"))); query.title.reset(new base::string16(ASCIIToUTF16("Google"))); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(1U, nodes.size()); EXPECT_TRUE(nodes[0] == node1); nodes.clear(); scoped_ptr* fields[] = { &query.word_phrase_query, &query.url, &query.title }; // Test two fields matching. for (size_t i = 0; i < arraysize(fields); i++) { scoped_ptr original_value(fields[i]->release()); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(1U, nodes.size()); EXPECT_TRUE(nodes[0] == node1); nodes.clear(); fields[i]->reset(original_value.release()); } // Test two fields matching with one non-matching field. for (size_t i = 0; i < arraysize(fields); i++) { scoped_ptr original_value(fields[i]->release()); fields[i]->reset(new base::string16(ASCIIToUTF16("fjdkslafjkldsa"))); GetBookmarksMatchingProperties(model.get(), query, 100, string(), &nodes); ASSERT_EQ(0U, nodes.size()); nodes.clear(); fields[i]->reset(original_value.release()); } } // Copy and paste is not yet supported on iOS. http://crbug.com/228147 #if !defined(OS_IOS) TEST_F(BookmarkUtilsTest, PasteBookmarkFromURL) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const base::string16 url_text = ASCIIToUTF16("http://www.google.com/"); const BookmarkNode* new_folder = model->AddFolder( model->bookmark_bar_node(), 0, ASCIIToUTF16("New_Folder")); // Write blank text to clipboard. { ui::ScopedClipboardWriter clipboard_writer(ui::CLIPBOARD_TYPE_COPY_PASTE); clipboard_writer.WriteText(base::string16()); } // Now we shouldn't be able to paste from the clipboard. EXPECT_FALSE(CanPasteFromClipboard(model.get(), new_folder)); // Write some valid url to the clipboard. { ui::ScopedClipboardWriter clipboard_writer(ui::CLIPBOARD_TYPE_COPY_PASTE); clipboard_writer.WriteText(url_text); } // Now we should be able to paste from the clipboard. EXPECT_TRUE(CanPasteFromClipboard(model.get(), new_folder)); PasteFromClipboard(model.get(), new_folder, 0); ASSERT_EQ(1, new_folder->child_count()); // Url for added node should be same as url_text. EXPECT_EQ(url_text, ASCIIToUTF16(new_folder->GetChild(0)->url().spec())); } TEST_F(BookmarkUtilsTest, CopyPaste) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const BookmarkNode* node = model->AddURL(model->other_node(), 0, ASCIIToUTF16("foo bar"), GURL("http://www.google.com")); // Copy a node to the clipboard. std::vector nodes; nodes.push_back(node); CopyToClipboard(model.get(), nodes, false); // And make sure we can paste a bookmark from the clipboard. EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node())); // Write some text to the clipboard. { ui::ScopedClipboardWriter clipboard_writer( ui::CLIPBOARD_TYPE_COPY_PASTE); clipboard_writer.WriteText(ASCIIToUTF16("foo")); } // Now we shouldn't be able to paste from the clipboard. EXPECT_FALSE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node())); } // Test for updating title such that url and title pair are unique among the // children of parent. TEST_F(BookmarkUtilsTest, MakeTitleUnique) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const base::string16 url_text = ASCIIToUTF16("http://www.google.com/"); const base::string16 title_text = ASCIIToUTF16("foobar"); const BookmarkNode* bookmark_bar_node = model->bookmark_bar_node(); const BookmarkNode* node = model->AddURL(bookmark_bar_node, 0, title_text, GURL(url_text)); EXPECT_EQ(url_text, ASCIIToUTF16(bookmark_bar_node->GetChild(0)->url().spec())); EXPECT_EQ(title_text, bookmark_bar_node->GetChild(0)->GetTitle()); // Copy a node to the clipboard. std::vector nodes; nodes.push_back(node); CopyToClipboard(model.get(), nodes, false); // Now we should be able to paste from the clipboard. EXPECT_TRUE(CanPasteFromClipboard(model.get(), bookmark_bar_node)); PasteFromClipboard(model.get(), bookmark_bar_node, 1); ASSERT_EQ(2, bookmark_bar_node->child_count()); // Url for added node should be same as url_text. EXPECT_EQ(url_text, ASCIIToUTF16(bookmark_bar_node->GetChild(1)->url().spec())); // Title for added node should be numeric subscript suffix with copied node // title. EXPECT_EQ(ASCIIToUTF16("foobar (1)"), bookmark_bar_node->GetChild(1)->GetTitle()); } TEST_F(BookmarkUtilsTest, CopyPasteMetaInfo) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); const BookmarkNode* node = model->AddURL(model->other_node(), 0, ASCIIToUTF16("foo bar"), GURL("http://www.google.com")); model->SetNodeMetaInfo(node, "somekey", "somevalue"); model->SetNodeMetaInfo(node, "someotherkey", "someothervalue"); // Copy a node to the clipboard. std::vector nodes; nodes.push_back(node); CopyToClipboard(model.get(), nodes, false); // Paste node to a different folder. const BookmarkNode* folder = model->AddFolder(model->bookmark_bar_node(), 0, ASCIIToUTF16("Folder")); EXPECT_EQ(0, folder->child_count()); // And make sure we can paste a bookmark from the clipboard. EXPECT_TRUE(CanPasteFromClipboard(model.get(), folder)); PasteFromClipboard(model.get(), folder, 0); ASSERT_EQ(1, folder->child_count()); // Verify that the pasted node contains the same meta info. const BookmarkNode* pasted = folder->GetChild(0); ASSERT_TRUE(pasted->GetMetaInfoMap()); EXPECT_EQ(2u, pasted->GetMetaInfoMap()->size()); std::string value; EXPECT_TRUE(pasted->GetMetaInfo("somekey", &value)); EXPECT_EQ("somevalue", value); EXPECT_TRUE(pasted->GetMetaInfo("someotherkey", &value)); EXPECT_EQ("someothervalue", value); } #if defined(OS_LINUX) || defined(OS_MACOSX) // http://crbug.com/396472 #define MAYBE_CutToClipboard DISABLED_CutToClipboard #else #define MAYBE_CutToClipboard CutToClipboard #endif TEST_F(BookmarkUtilsTest, MAYBE_CutToClipboard) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); model->AddObserver(this); base::string16 title(ASCIIToUTF16("foo")); GURL url("http://foo.com"); const BookmarkNode* n1 = model->AddURL(model->other_node(), 0, title, url); const BookmarkNode* n2 = model->AddURL(model->other_node(), 1, title, url); // Cut the nodes to the clipboard. std::vector nodes; nodes.push_back(n1); nodes.push_back(n2); CopyToClipboard(model.get(), nodes, true); // Make sure the nodes were removed. EXPECT_EQ(0, model->other_node()->child_count()); // Make sure observers were notified the set of changes should be grouped. ExpectGroupedChangeCount(1, 1); // And make sure we can paste from the clipboard. EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->other_node())); } TEST_F(BookmarkUtilsTest, PasteNonEditableNodes) { TestBookmarkClient client; // Load a model with an extra node that is not editable. BookmarkPermanentNode* extra_node = new BookmarkPermanentNode(100); BookmarkPermanentNodeList extra_nodes; extra_nodes.push_back(extra_node); client.SetExtraNodesToLoad(extra_nodes.Pass()); scoped_ptr model(client.CreateModel()); const BookmarkNode* node = model->AddURL(model->other_node(), 0, ASCIIToUTF16("foo bar"), GURL("http://www.google.com")); // Copy a node to the clipboard. std::vector nodes; nodes.push_back(node); CopyToClipboard(model.get(), nodes, false); // And make sure we can paste a bookmark from the clipboard. EXPECT_TRUE(CanPasteFromClipboard(model.get(), model->bookmark_bar_node())); // But it can't be pasted into a non-editable folder. BookmarkClient* upcast = &client; EXPECT_FALSE(upcast->CanBeEditedByUser(extra_node)); EXPECT_FALSE(CanPasteFromClipboard(model.get(), extra_node)); } #endif // !defined(OS_IOS) TEST_F(BookmarkUtilsTest, GetParentForNewNodes) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); // This tests the case where selection contains one item and that item is a // folder. std::vector nodes; nodes.push_back(model->bookmark_bar_node()); int index = -1; const BookmarkNode* real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); EXPECT_EQ(real_parent, model->bookmark_bar_node()); EXPECT_EQ(0, index); nodes.clear(); // This tests the case where selection contains one item and that item is an // url. const BookmarkNode* page1 = model->AddURL(model->bookmark_bar_node(), 0, ASCIIToUTF16("Google"), GURL("http://google.com")); nodes.push_back(page1); real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); EXPECT_EQ(real_parent, model->bookmark_bar_node()); EXPECT_EQ(1, index); // This tests the case where selection has more than one item. const BookmarkNode* folder1 = model->AddFolder(model->bookmark_bar_node(), 1, ASCIIToUTF16("Folder 1")); nodes.push_back(folder1); real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); EXPECT_EQ(real_parent, model->bookmark_bar_node()); EXPECT_EQ(2, index); // This tests the case where selection doesn't contain any items. nodes.clear(); real_parent = GetParentForNewNodes(model->bookmark_bar_node(), nodes, &index); EXPECT_EQ(real_parent, model->bookmark_bar_node()); EXPECT_EQ(2, index); } // Verifies that meta info is copied when nodes are cloned. TEST_F(BookmarkUtilsTest, CloneMetaInfo) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); // Add a node containing meta info. const BookmarkNode* node = model->AddURL(model->other_node(), 0, ASCIIToUTF16("foo bar"), GURL("http://www.google.com")); model->SetNodeMetaInfo(node, "somekey", "somevalue"); model->SetNodeMetaInfo(node, "someotherkey", "someothervalue"); // Clone node to a different folder. const BookmarkNode* folder = model->AddFolder(model->bookmark_bar_node(), 0, ASCIIToUTF16("Folder")); std::vector elements; BookmarkNodeData::Element node_data(node); elements.push_back(node_data); EXPECT_EQ(0, folder->child_count()); CloneBookmarkNode(model.get(), elements, folder, 0, false); ASSERT_EQ(1, folder->child_count()); // Verify that the cloned node contains the same meta info. const BookmarkNode* clone = folder->GetChild(0); ASSERT_TRUE(clone->GetMetaInfoMap()); EXPECT_EQ(2u, clone->GetMetaInfoMap()->size()); std::string value; EXPECT_TRUE(clone->GetMetaInfo("somekey", &value)); EXPECT_EQ("somevalue", value); EXPECT_TRUE(clone->GetMetaInfo("someotherkey", &value)); EXPECT_EQ("someothervalue", value); } // Verifies that meta info fields in the non cloned set are not copied when // cloning a bookmark. TEST_F(BookmarkUtilsTest, CloneBookmarkResetsNonClonedKey) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); model->AddNonClonedKey("foo"); const BookmarkNode* parent = model->other_node(); const BookmarkNode* node = model->AddURL( parent, 0, ASCIIToUTF16("title"), GURL("http://www.google.com")); model->SetNodeMetaInfo(node, "foo", "ignored value"); model->SetNodeMetaInfo(node, "bar", "kept value"); std::vector elements; BookmarkNodeData::Element node_data(node); elements.push_back(node_data); // Cloning a bookmark should clear the non cloned key. CloneBookmarkNode(model.get(), elements, parent, 0, true); ASSERT_EQ(2, parent->child_count()); std::string value; EXPECT_FALSE(parent->GetChild(0)->GetMetaInfo("foo", &value)); // Other keys should still be cloned. EXPECT_TRUE(parent->GetChild(0)->GetMetaInfo("bar", &value)); EXPECT_EQ("kept value", value); } // Verifies that meta info fields in the non cloned set are not copied when // cloning a folder. TEST_F(BookmarkUtilsTest, CloneFolderResetsNonClonedKey) { TestBookmarkClient client; scoped_ptr model(client.CreateModel()); model->AddNonClonedKey("foo"); const BookmarkNode* parent = model->other_node(); const BookmarkNode* node = model->AddFolder(parent, 0, ASCIIToUTF16("title")); model->SetNodeMetaInfo(node, "foo", "ignored value"); model->SetNodeMetaInfo(node, "bar", "kept value"); std::vector elements; BookmarkNodeData::Element node_data(node); elements.push_back(node_data); // Cloning a folder should clear the non cloned key. CloneBookmarkNode(model.get(), elements, parent, 0, true); ASSERT_EQ(2, parent->child_count()); std::string value; EXPECT_FALSE(parent->GetChild(0)->GetMetaInfo("foo", &value)); // Other keys should still be cloned. EXPECT_TRUE(parent->GetChild(0)->GetMetaInfo("bar", &value)); EXPECT_EQ("kept value", value); } TEST_F(BookmarkUtilsTest, RemoveAllBookmarks) { TestBookmarkClient client; // Load a model with an extra node that is not editable. BookmarkPermanentNode* extra_node = new BookmarkPermanentNode(100); BookmarkPermanentNodeList extra_nodes; extra_nodes.push_back(extra_node); client.SetExtraNodesToLoad(extra_nodes.Pass()); scoped_ptr model(client.CreateModel()); EXPECT_TRUE(model->bookmark_bar_node()->empty()); EXPECT_TRUE(model->other_node()->empty()); EXPECT_TRUE(model->mobile_node()->empty()); EXPECT_TRUE(extra_node->empty()); const base::string16 title = base::ASCIIToUTF16("Title"); const GURL url("http://google.com"); model->AddURL(model->bookmark_bar_node(), 0, title, url); model->AddURL(model->other_node(), 0, title, url); model->AddURL(model->mobile_node(), 0, title, url); model->AddURL(extra_node, 0, title, url); std::vector nodes; model->GetNodesByURL(url, &nodes); ASSERT_EQ(4u, nodes.size()); RemoveAllBookmarks(model.get(), url); nodes.clear(); model->GetNodesByURL(url, &nodes); ASSERT_EQ(1u, nodes.size()); EXPECT_TRUE(model->bookmark_bar_node()->empty()); EXPECT_TRUE(model->other_node()->empty()); EXPECT_TRUE(model->mobile_node()->empty()); EXPECT_EQ(1, extra_node->child_count()); } } // namespace } // namespace bookmarks