1 // Copyright 2018 The Chromium Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #include "components/sync_bookmarks/bookmark_model_observer_impl.h"
14 #include "base/callback_helpers.h"
15 #include "base/strings/utf_string_conversions.h"
16 #include "base/test/mock_callback.h"
17 #include "components/bookmarks/browser/bookmark_model.h"
18 #include "components/bookmarks/common/bookmark_metrics.h"
19 #include "components/bookmarks/test/test_bookmark_client.h"
20 #include "components/favicon_base/favicon_types.h"
21 #include "components/sync/base/time.h"
22 #include "components/sync/base/unique_position.h"
23 #include "components/sync/protocol/bookmark_specifics.pb.h"
24 #include "components/sync/protocol/entity_metadata.pb.h"
25 #include "components/sync/protocol/entity_specifics.pb.h"
26 #include "components/sync/protocol/model_type_state.pb.h"
27 #include "components/sync_bookmarks/bookmark_specifics_conversions.h"
28 #include "components/sync_bookmarks/synced_bookmark_tracker.h"
29 #include "components/sync_bookmarks/synced_bookmark_tracker_entity.h"
30 #include "components/undo/bookmark_undo_service.h"
31 #include "testing/gmock/include/gmock/gmock.h"
32 #include "testing/gtest/include/gtest/gtest.h"
33 #include "third_party/skia/include/core/SkBitmap.h"
34 #include "ui/gfx/image/image.h"
36 namespace sync_bookmarks {
40 using testing::ElementsAre;
42 using testing::IsEmpty;
43 using testing::IsNull;
45 using testing::NiceMock;
46 using testing::NotNull;
47 using testing::SizeIs;
48 using testing::UnorderedElementsAre;
50 const char kBookmarkBarId[] = "bookmark_bar_id";
51 const char kBookmarkBarTag[] = "bookmark_bar";
52 const char kOtherBookmarksId[] = "other_bookmarks_id";
53 const char kOtherBookmarksTag[] = "other_bookmarks";
54 const char kMobileBookmarksId[] = "synced_bookmarks_id";
55 const char kMobileBookmarksTag[] = "synced_bookmarks";
57 // Matches |arg| of type SyncedBookmarkTrackerEntity*.
58 MATCHER_P(HasBookmarkNode, node, "") {
59 return arg->bookmark_node() == node;
62 // Returns a single-color 16x16 image using |color|.
63 gfx::Image CreateTestImage(SkColor color) {
65 bitmap.allocN32Pixels(16, 16);
66 bitmap.eraseColor(color);
67 return gfx::Image::CreateFrom1xBitmap(bitmap);
70 class BookmarkModelObserverImplTest : public testing::Test {
72 BookmarkModelObserverImplTest()
74 SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState())),
75 observer_(nudge_for_commit_closure_.Get(),
76 /*on_bookmark_model_being_deleted_closure=*/base::DoNothing(),
77 bookmark_tracker_.get()),
78 bookmark_model_(bookmarks::TestBookmarkClient::CreateModel()) {
79 bookmark_model_->AddObserver(&observer_);
80 sync_pb::EntitySpecifics specifics;
81 specifics.mutable_bookmark()->set_legacy_canonicalized_title(
83 bookmark_tracker_->Add(
84 /*bookmark_node=*/bookmark_model()->bookmark_bar_node(),
85 /*sync_id=*/kBookmarkBarId,
86 /*server_version=*/0, /*creation_time=*/base::Time::Now(), specifics);
87 specifics.mutable_bookmark()->set_legacy_canonicalized_title(
89 bookmark_tracker_->Add(
90 /*bookmark_node=*/bookmark_model()->other_node(),
91 /*sync_id=*/kOtherBookmarksId,
92 /*server_version=*/0, /*creation_time=*/base::Time::Now(), specifics);
93 specifics.mutable_bookmark()->set_legacy_canonicalized_title(
95 bookmark_tracker_->Add(
96 /*bookmark_node=*/bookmark_model()->mobile_node(),
97 /*sync_id=*/kMobileBookmarksId,
98 /*server_version=*/0, /*creation_time=*/base::Time::Now(), specifics);
101 ~BookmarkModelObserverImplTest() override {
102 bookmark_model_->RemoveObserver(&observer_);
105 void SimulateCommitResponseForAllLocalChanges() {
106 for (const SyncedBookmarkTrackerEntity* entity :
107 bookmark_tracker()->GetEntitiesWithLocalChanges()) {
108 const std::string id = entity->metadata().server_id();
109 // Don't simulate change in id for simplicity.
110 bookmark_tracker()->UpdateUponCommitResponse(
112 /*server_version=*/1,
113 /*acked_sequence_number=*/entity->metadata().sequence_number());
117 syncer::UniquePosition PositionOf(
118 const bookmarks::BookmarkNode* bookmark_node) {
119 const SyncedBookmarkTrackerEntity* entity =
120 bookmark_tracker()->GetEntityForBookmarkNode(bookmark_node);
121 return syncer::UniquePosition::FromProto(
122 entity->metadata().unique_position());
125 std::vector<const bookmarks::BookmarkNode*> GenerateBookmarkNodes(
126 size_t num_bookmarks) {
127 const std::string kTitle = "title";
128 const std::string kUrl = "http://www.url.com";
130 const bookmarks::BookmarkNode* bookmark_bar_node =
131 bookmark_model()->bookmark_bar_node();
132 std::vector<const bookmarks::BookmarkNode*> nodes;
133 for (size_t i = 0; i < num_bookmarks; ++i) {
134 nodes.push_back(bookmark_model()->AddURL(
135 /*parent=*/bookmark_bar_node, /*index=*/i, base::UTF8ToUTF16(kTitle),
139 // Verify number of entities local changes. Should be the same as number of
141 DCHECK_EQ(bookmark_tracker()->GetEntitiesWithLocalChanges().size(),
144 // All bookmarks should be tracked now (including permanent nodes).
145 DCHECK_EQ(bookmark_tracker()->TrackedEntitiesCountForTest(),
151 bookmarks::BookmarkModel* bookmark_model() { return bookmark_model_.get(); }
152 SyncedBookmarkTracker* bookmark_tracker() { return bookmark_tracker_.get(); }
153 BookmarkModelObserverImpl* observer() { return &observer_; }
154 base::MockCallback<base::RepeatingClosure>* nudge_for_commit_closure() {
155 return &nudge_for_commit_closure_;
157 bookmarks::TestBookmarkClient* bookmark_client() {
158 return static_cast<bookmarks::TestBookmarkClient*>(
159 bookmark_model_->client());
163 NiceMock<base::MockCallback<base::RepeatingClosure>>
164 nudge_for_commit_closure_;
165 std::unique_ptr<SyncedBookmarkTracker> bookmark_tracker_;
166 BookmarkModelObserverImpl observer_;
167 std::unique_ptr<bookmarks::BookmarkModel> bookmark_model_;
170 TEST_F(BookmarkModelObserverImplTest,
171 BookmarkAddedShouldPutInTheTrackerAndNudgeForCommit) {
172 const std::string kTitle = "title";
173 const std::string kUrl = "http://www.url.com";
175 EXPECT_CALL(*nudge_for_commit_closure(), Run());
176 const bookmarks::BookmarkNode* bookmark_bar_node =
177 bookmark_model()->bookmark_bar_node();
178 const bookmarks::BookmarkNode* bookmark_node = bookmark_model()->AddURL(
179 /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle),
182 EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 4U);
184 std::vector<const SyncedBookmarkTrackerEntity*> local_changes =
185 bookmark_tracker()->GetEntitiesWithLocalChanges();
186 ASSERT_THAT(local_changes.size(), 1U);
187 EXPECT_THAT(local_changes[0]->bookmark_node(), Eq(bookmark_node));
188 EXPECT_THAT(local_changes[0]->metadata().server_id(),
189 Eq(bookmark_node->guid().AsLowercaseString()));
192 TEST_F(BookmarkModelObserverImplTest,
193 BookmarkChangedShouldUpdateTheTrackerAndNudgeForCommit) {
194 const std::string kTitle1 = "title1";
195 const std::string kUrl1 = "http://www.url1.com";
196 const std::string kNewUrl1 = "http://www.new-url1.com";
197 const std::string kTitle2 = "title2";
198 const std::string kUrl2 = "http://www.url2.com";
199 const std::string kNewTitle2 = "new_title2";
201 const bookmarks::BookmarkNode* bookmark_bar_node =
202 bookmark_model()->bookmark_bar_node();
203 const bookmarks::BookmarkNode* bookmark_node1 = bookmark_model()->AddURL(
204 /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle1),
206 const bookmarks::BookmarkNode* bookmark_node2 = bookmark_model()->AddURL(
207 /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle2),
209 // Both bookmarks should be tracked now.
210 ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 5U);
211 // There should be two local changes now for both entities.
212 ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 2U);
214 SimulateCommitResponseForAllLocalChanges();
216 // There should be no local changes now.
217 ASSERT_TRUE(bookmark_tracker()->GetEntitiesWithLocalChanges().empty());
219 // Now update the title of the 2nd node.
220 EXPECT_CALL(*nudge_for_commit_closure(), Run());
221 bookmark_model()->SetTitle(bookmark_node2, base::UTF8ToUTF16(kNewTitle2),
222 bookmarks::metrics::BookmarkEditSource::kOther);
223 // Node 2 should be in the local changes list.
224 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
225 ElementsAre(HasBookmarkNode(bookmark_node2)));
227 // Now update the url of the 1st node.
228 EXPECT_CALL(*nudge_for_commit_closure(), Run());
229 bookmark_model()->SetURL(bookmark_node1, GURL(kNewUrl1),
230 bookmarks::metrics::BookmarkEditSource::kOther);
232 // Node 1 and 2 should be in the local changes list.
233 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
234 UnorderedElementsAre(HasBookmarkNode(bookmark_node1),
235 HasBookmarkNode(bookmark_node2)));
237 // Now update metainfo of the 1st node.
238 EXPECT_CALL(*nudge_for_commit_closure(), Run());
239 bookmark_model()->SetNodeMetaInfo(bookmark_node1, "key", "value");
242 TEST_F(BookmarkModelObserverImplTest,
243 BookmarkMovedShouldUpdateTheTrackerAndNudgeForCommit) {
244 // Build this structure:
248 const GURL kUrl("http://www.url1.com");
250 const bookmarks::BookmarkNode* bookmark_bar_node =
251 bookmark_model()->bookmark_bar_node();
252 const bookmarks::BookmarkNode* folder1_node = bookmark_model()->AddFolder(
253 /*parent=*/bookmark_bar_node, /*index=*/0, u"folder1");
254 const bookmarks::BookmarkNode* bookmark1_node = bookmark_model()->AddURL(
255 /*parent=*/folder1_node, /*index=*/0, u"bookmark1", kUrl);
257 // Verify number of entities local changes. Should be the same as number of
259 ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 2U);
261 // All bookmarks should be tracked now.
262 ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 5U);
264 SimulateCommitResponseForAllLocalChanges();
266 // There should be no local changes now.
267 ASSERT_TRUE(bookmark_tracker()->GetEntitiesWithLocalChanges().empty());
269 // Now change it to this structure.
270 // Build this structure:
275 EXPECT_CALL(*nudge_for_commit_closure(), Run());
276 bookmark_model()->Move(bookmark1_node, bookmark_bar_node, 0);
277 EXPECT_TRUE(PositionOf(bookmark1_node).LessThan(PositionOf(folder1_node)));
280 TEST_F(BookmarkModelObserverImplTest,
281 ReorderChildrenShouldUpdateTheTrackerAndNudgeForCommit) {
282 std::vector<const bookmarks::BookmarkNode*> nodes =
283 GenerateBookmarkNodes(/*num_bookmarks=*/4);
285 SimulateCommitResponseForAllLocalChanges();
287 // Reorder it to be (2 bookmarks have been moved):
293 bookmark_model()->ReorderChildren(bookmark_model()->bookmark_bar_node(),
294 {nodes[1], nodes[3], nodes[0], nodes[2]});
295 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[3])));
296 EXPECT_TRUE(PositionOf(nodes[3]).LessThan(PositionOf(nodes[0])));
297 EXPECT_TRUE(PositionOf(nodes[0]).LessThan(PositionOf(nodes[2])));
299 // Only 2 moved nodes should have local changes to commit.
300 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), SizeIs(2));
303 TEST_F(BookmarkModelObserverImplTest,
304 ShouldReorderChildrenAndUpdateOnlyMovedToRightBookmark) {
305 std::vector<const bookmarks::BookmarkNode*> nodes =
306 GenerateBookmarkNodes(/*num_bookmarks=*/4);
308 SimulateCommitResponseForAllLocalChanges();
316 bookmark_model()->ReorderChildren(bookmark_model()->bookmark_bar_node(),
317 {nodes[1], nodes[2], nodes[0], nodes[3]});
318 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[2])));
319 EXPECT_TRUE(PositionOf(nodes[2]).LessThan(PositionOf(nodes[0])));
320 EXPECT_TRUE(PositionOf(nodes[0]).LessThan(PositionOf(nodes[3])));
322 // Only one moved node should have local changes to commit.
323 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
324 UnorderedElementsAre(HasBookmarkNode(nodes[0])));
327 TEST_F(BookmarkModelObserverImplTest,
328 ShouldReorderChildrenAndUpdateOnlyMovedToLeftBookmark) {
329 std::vector<const bookmarks::BookmarkNode*> nodes =
330 GenerateBookmarkNodes(/*num_bookmarks=*/4);
332 SimulateCommitResponseForAllLocalChanges();
340 bookmark_model()->ReorderChildren(bookmark_model()->bookmark_bar_node(),
341 {nodes[0], nodes[3], nodes[1], nodes[2]});
342 EXPECT_TRUE(PositionOf(nodes[0]).LessThan(PositionOf(nodes[3])));
343 EXPECT_TRUE(PositionOf(nodes[3]).LessThan(PositionOf(nodes[1])));
344 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[2])));
346 // Only one moved node should have local changes to commit.
347 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
348 UnorderedElementsAre(HasBookmarkNode(nodes[3])));
351 TEST_F(BookmarkModelObserverImplTest,
352 ShouldReorderWhenBookmarkMovedToLastPosition) {
353 std::vector<const bookmarks::BookmarkNode*> nodes =
354 GenerateBookmarkNodes(/*num_bookmarks=*/4);
356 SimulateCommitResponseForAllLocalChanges();
364 bookmark_model()->ReorderChildren(bookmark_model()->bookmark_bar_node(),
365 {nodes[1], nodes[2], nodes[3], nodes[0]});
366 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[2])));
367 EXPECT_TRUE(PositionOf(nodes[2]).LessThan(PositionOf(nodes[3])));
368 EXPECT_TRUE(PositionOf(nodes[3]).LessThan(PositionOf(nodes[0])));
370 // Only one moved node should have local changes to commit.
371 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
372 UnorderedElementsAre(HasBookmarkNode(nodes[0])));
375 TEST_F(BookmarkModelObserverImplTest,
376 ShouldReorderWhenBookmarkMovedToFirstPosition) {
377 std::vector<const bookmarks::BookmarkNode*> nodes =
378 GenerateBookmarkNodes(/*num_bookmarks=*/4);
380 SimulateCommitResponseForAllLocalChanges();
388 bookmark_model()->ReorderChildren(bookmark_model()->bookmark_bar_node(),
389 {nodes[3], nodes[0], nodes[1], nodes[2]});
390 EXPECT_TRUE(PositionOf(nodes[3]).LessThan(PositionOf(nodes[0])));
391 EXPECT_TRUE(PositionOf(nodes[0]).LessThan(PositionOf(nodes[1])));
392 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[2])));
394 // Only one moved node should have local changes to commit.
395 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
396 UnorderedElementsAre(HasBookmarkNode(nodes[3])));
399 TEST_F(BookmarkModelObserverImplTest, ShouldReorderWhenAllBookmarksReversed) {
400 // In this case almost all the bookmarks should be updated apart from only one
402 std::vector<const bookmarks::BookmarkNode*> nodes =
403 GenerateBookmarkNodes(/*num_bookmarks=*/4);
405 SimulateCommitResponseForAllLocalChanges();
407 // Reorder it to be (all nodes are moved):
413 bookmark_model()->ReorderChildren(bookmark_model()->bookmark_bar_node(),
414 {nodes[3], nodes[2], nodes[1], nodes[0]});
415 EXPECT_TRUE(PositionOf(nodes[3]).LessThan(PositionOf(nodes[2])));
416 EXPECT_TRUE(PositionOf(nodes[2]).LessThan(PositionOf(nodes[1])));
417 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[0])));
419 // Do not verify which nodes exactly have been updated, it depends on the
420 // implementation and any of the nodes may become a base node to calculate
421 // relative positions of all other nodes.
422 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), SizeIs(3));
425 TEST_F(BookmarkModelObserverImplTest,
426 ShouldNotReorderIfAllBookmarksStillOrdered) {
427 std::vector<const bookmarks::BookmarkNode*> nodes =
428 GenerateBookmarkNodes(/*num_bookmarks=*/4);
430 SimulateCommitResponseForAllLocalChanges();
432 // Keep the original order.
433 bookmark_model()->ReorderChildren(bookmark_model()->bookmark_bar_node(),
434 {nodes[0], nodes[1], nodes[2], nodes[3]});
435 EXPECT_TRUE(PositionOf(nodes[0]).LessThan(PositionOf(nodes[1])));
436 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[2])));
437 EXPECT_TRUE(PositionOf(nodes[2]).LessThan(PositionOf(nodes[3])));
439 // The bookmarks remain in the same order, nothing to commit.
440 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), IsEmpty());
443 TEST_F(BookmarkModelObserverImplTest,
444 BookmarkRemovalShouldUpdateTheTrackerAndNudgeForCommit) {
445 // Build this structure:
453 // and then delete folder2.
454 const GURL kUrl("http://www.url1.com");
456 const bookmarks::BookmarkNode* bookmark_bar_node =
457 bookmark_model()->bookmark_bar_node();
458 const bookmarks::BookmarkNode* folder1_node = bookmark_model()->AddFolder(
459 /*parent=*/bookmark_bar_node, /*index=*/0, u"folder1");
460 const bookmarks::BookmarkNode* bookmark1_node = bookmark_model()->AddURL(
461 /*parent=*/folder1_node, /*index=*/0, u"bookmark1", kUrl);
462 const bookmarks::BookmarkNode* folder2_node = bookmark_model()->AddFolder(
463 /*parent=*/folder1_node, /*index=*/1, u"folder2");
464 const bookmarks::BookmarkNode* bookmark2_node = bookmark_model()->AddURL(
465 /*parent=*/folder2_node, /*index=*/0, u"bookmark2", kUrl);
466 const bookmarks::BookmarkNode* bookmark3_node = bookmark_model()->AddURL(
467 /*parent=*/folder2_node, /*index=*/1, u"bookmark3", kUrl);
469 // All bookmarks should be tracked now.
470 ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 8U);
472 SimulateCommitResponseForAllLocalChanges();
474 // There should be no local changes now.
475 ASSERT_TRUE(bookmark_tracker()->GetEntitiesWithLocalChanges().empty());
477 const SyncedBookmarkTrackerEntity* folder2_entity =
478 bookmark_tracker()->GetEntityForBookmarkNode(folder2_node);
479 const SyncedBookmarkTrackerEntity* bookmark2_entity =
480 bookmark_tracker()->GetEntityForBookmarkNode(bookmark2_node);
481 const SyncedBookmarkTrackerEntity* bookmark3_entity =
482 bookmark_tracker()->GetEntityForBookmarkNode(bookmark3_node);
484 ASSERT_FALSE(folder2_entity->metadata().is_deleted());
485 ASSERT_FALSE(bookmark2_entity->metadata().is_deleted());
486 ASSERT_FALSE(bookmark3_entity->metadata().is_deleted());
488 const std::string& folder2_entity_id = folder2_entity->metadata().server_id();
489 const std::string& bookmark2_entity_id =
490 bookmark2_entity->metadata().server_id();
491 const std::string& bookmark3_entity_id =
492 bookmark3_entity->metadata().server_id();
494 EXPECT_CALL(*nudge_for_commit_closure(), Run());
495 bookmark_model()->Remove(folder2_node);
497 // folder2, bookmark2, and bookmark3 should be marked deleted.
498 EXPECT_TRUE(bookmark_tracker()
499 ->GetEntityForSyncId(folder2_entity_id)
502 EXPECT_TRUE(bookmark_tracker()
503 ->GetEntityForSyncId(bookmark2_entity_id)
506 EXPECT_TRUE(bookmark_tracker()
507 ->GetEntityForSyncId(bookmark3_entity_id)
511 // folder2, bookmark2, and bookmark3 should be in the local changes to be
512 // committed and folder2 deletion should be the last one (after all children
515 bookmark_tracker()->GetEntitiesWithLocalChanges(),
516 ElementsAre(bookmark_tracker()->GetEntityForSyncId(bookmark2_entity_id),
517 bookmark_tracker()->GetEntityForSyncId(bookmark3_entity_id),
518 bookmark_tracker()->GetEntityForSyncId(folder2_entity_id)));
520 // folder1 and bookmark1 are still tracked.
521 EXPECT_TRUE(bookmark_tracker()->GetEntityForBookmarkNode(folder1_node));
522 EXPECT_TRUE(bookmark_tracker()->GetEntityForBookmarkNode(bookmark1_node));
525 TEST_F(BookmarkModelObserverImplTest,
526 BookmarkCreationAndRemovalShouldRequireTwoCommitResponsesBeforeRemoval) {
527 const bookmarks::BookmarkNode* bookmark_bar_node =
528 bookmark_model()->bookmark_bar_node();
529 const bookmarks::BookmarkNode* folder_node = bookmark_model()->AddFolder(
530 /*parent=*/bookmark_bar_node, /*index=*/0, u"folder");
532 // Node should be tracked now.
533 ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 4U);
534 const SyncedBookmarkTrackerEntity* entity =
535 bookmark_tracker()->GetEntityForBookmarkNode(folder_node);
536 const std::string id = entity->metadata().server_id();
537 ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 1U);
539 bookmark_tracker()->MarkCommitMayHaveStarted(entity);
541 // Remove the folder.
542 bookmark_model()->Remove(folder_node);
544 // Simulate a commit response for the first commit request (the creation).
545 // Don't simulate change in id for simplicity.
546 bookmark_tracker()->UpdateUponCommitResponse(entity, id,
547 /*server_version=*/1,
548 /*acked_sequence_number=*/1);
550 // There should still be one local change (the deletion).
551 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 1U);
553 // Entity is still tracked.
554 EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 4U);
556 // Commit the deletion.
557 bookmark_tracker()->UpdateUponCommitResponse(entity, id,
558 /*server_version=*/2,
559 /*acked_sequence_number=*/2);
560 // Entity should have been dropped.
561 EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 3U);
564 TEST_F(BookmarkModelObserverImplTest,
565 BookmarkCreationAndRemovalBeforeCommitRequestShouldBeRemovedDirectly) {
566 const bookmarks::BookmarkNode* bookmark_bar_node =
567 bookmark_model()->bookmark_bar_node();
568 const bookmarks::BookmarkNode* folder_node = bookmark_model()->AddFolder(
569 /*parent=*/bookmark_bar_node, /*index=*/0, u"folder");
571 // Node should be tracked now.
572 ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 4U);
573 const std::string id = bookmark_tracker()
574 ->GetEntityForBookmarkNode(folder_node)
577 ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 1U);
579 // Remove the folder.
580 bookmark_model()->Remove(folder_node);
582 // Entity should have been dropped.
583 EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 3U);
586 TEST_F(BookmarkModelObserverImplTest, ShouldPositionSiblings) {
587 const std::string kTitle = "title";
588 const std::string kUrl = "http://www.url.com";
590 // Build this structure:
597 const bookmarks::BookmarkNode* bookmark_bar_node =
598 bookmark_model()->bookmark_bar_node();
599 const bookmarks::BookmarkNode* bookmark_node1 = bookmark_model()->AddURL(
600 /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle),
603 const bookmarks::BookmarkNode* bookmark_node2 = bookmark_model()->AddURL(
604 /*parent=*/bookmark_bar_node, /*index=*/1, base::UTF8ToUTF16(kTitle),
607 EXPECT_TRUE(PositionOf(bookmark_node1).LessThan(PositionOf(bookmark_node2)));
609 // Now insert node3 at index 1 to build this structure:
615 // p1 < p2 (still holds)
619 const bookmarks::BookmarkNode* bookmark_node3 = bookmark_model()->AddURL(
620 /*parent=*/bookmark_bar_node, /*index=*/1, base::UTF8ToUTF16(kTitle),
622 EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), Eq(6U));
624 EXPECT_TRUE(PositionOf(bookmark_node1).LessThan(PositionOf(bookmark_node2)));
625 EXPECT_TRUE(PositionOf(bookmark_node1).LessThan(PositionOf(bookmark_node3)));
626 EXPECT_TRUE(PositionOf(bookmark_node3).LessThan(PositionOf(bookmark_node2)));
629 TEST_F(BookmarkModelObserverImplTest, ShouldNotSyncUnsyncableBookmarks) {
630 auto client = std::make_unique<bookmarks::TestBookmarkClient>();
631 bookmarks::BookmarkNode* managed_node = client->EnableManagedNode();
633 std::unique_ptr<bookmarks::BookmarkModel> model =
634 bookmarks::TestBookmarkClient::CreateModelWithClient(std::move(client));
636 std::unique_ptr<SyncedBookmarkTracker> bookmark_tracker =
637 SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
638 sync_pb::EntitySpecifics specifics;
639 specifics.mutable_bookmark()->set_legacy_canonicalized_title(kBookmarkBarTag);
640 *specifics.mutable_bookmark()->mutable_unique_position() =
641 syncer::UniquePosition::InitialPosition(
642 syncer::UniquePosition::RandomSuffix())
644 bookmark_tracker->Add(
645 /*bookmark_node=*/model->bookmark_bar_node(),
646 /*sync_id=*/kBookmarkBarId,
647 /*server_version=*/0, /*creation_time=*/base::Time::Now(), specifics);
648 specifics.mutable_bookmark()->set_legacy_canonicalized_title(
650 bookmark_tracker->Add(
651 /*bookmark_node=*/model->other_node(),
652 /*sync_id=*/kOtherBookmarksId,
653 /*server_version=*/0, /*creation_time=*/base::Time::Now(), specifics);
654 specifics.mutable_bookmark()->set_legacy_canonicalized_title(
655 kMobileBookmarksTag);
656 bookmark_tracker->Add(
657 /*bookmark_node=*/model->mobile_node(),
658 /*sync_id=*/kMobileBookmarksId,
659 /*server_version=*/0, /*creation_time=*/base::Time::Now(), specifics);
660 BookmarkModelObserverImpl observer(
661 nudge_for_commit_closure()->Get(),
662 /*on_bookmark_model_being_deleted_closure=*/base::DoNothing(),
663 bookmark_tracker.get());
665 model->AddObserver(&observer);
667 EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
668 // In the TestBookmarkClient, descendants of managed nodes shouldn't be
670 const bookmarks::BookmarkNode* unsyncable_node =
671 model->AddURL(/*parent=*/managed_node, /*index=*/0, u"Title",
672 GURL("http://www.url.com"));
673 // Only permanent folders should be tracked.
674 EXPECT_THAT(bookmark_tracker->TrackedEntitiesCountForTest(), 3U);
676 EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
677 // In the TestBookmarkClient, descendants of managed nodes shouldn't be
679 model->SetTitle(unsyncable_node, u"NewTitle",
680 bookmarks::metrics::BookmarkEditSource::kOther);
681 // Only permanent folders should be tracked.
682 EXPECT_THAT(bookmark_tracker->TrackedEntitiesCountForTest(), 3U);
684 EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
685 // In the TestBookmarkClient, descendants of managed nodes shouldn't be
687 model->Remove(unsyncable_node);
689 // Only permanent folders should be tracked.
690 EXPECT_THAT(bookmark_tracker->TrackedEntitiesCountForTest(), 3U);
691 model->RemoveObserver(&observer);
694 TEST_F(BookmarkModelObserverImplTest, ShouldAddChildrenInArbitraryOrder) {
695 std::unique_ptr<SyncedBookmarkTracker> bookmark_tracker =
696 SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
697 BookmarkModelObserverImpl observer(
698 /*nudge_for_commit_closure=*/base::DoNothing(),
699 /*on_bookmark_model_being_deleted_closure=*/base::DoNothing(),
700 bookmark_tracker.get());
701 const bookmarks::BookmarkNode* bookmark_bar_node =
702 bookmark_model()->bookmark_bar_node();
703 // Add the bookmark bar to the tracker.
704 sync_pb::EntitySpecifics specifics;
705 specifics.mutable_bookmark()->set_legacy_canonicalized_title(kBookmarkBarTag);
706 *specifics.mutable_bookmark()->mutable_unique_position() =
707 syncer::UniquePosition::InitialPosition(
708 syncer::UniquePosition::RandomSuffix())
710 bookmark_tracker->Add(
711 /*bookmark_node=*/bookmark_model()->bookmark_bar_node(),
712 /*sync_id=*/kBookmarkBarId,
713 /*server_version=*/0, /*creation_time=*/base::Time::Now(), specifics);
715 // Build this structure:
723 const bookmarks::BookmarkNode* nodes[5];
724 for (size_t i = 0; i < 5; i++) {
725 nodes[i] = bookmark_model()->AddFolder(
726 /*parent=*/bookmark_bar_node, /*index=*/i,
727 base::UTF8ToUTF16("folder" + std::to_string(i)));
730 // Now simulate calling the observer as if the nodes are added in that order.
732 observer.BookmarkNodeAdded(bookmark_model(), bookmark_bar_node, 4, false);
733 observer.BookmarkNodeAdded(bookmark_model(), bookmark_bar_node, 0, false);
734 observer.BookmarkNodeAdded(bookmark_model(), bookmark_bar_node, 2, false);
735 observer.BookmarkNodeAdded(bookmark_model(), bookmark_bar_node, 3, false);
736 observer.BookmarkNodeAdded(bookmark_model(), bookmark_bar_node, 1, false);
738 ASSERT_THAT(bookmark_tracker->TrackedEntitiesCountForTest(), 6U);
740 // Check that position information match the children order.
741 EXPECT_TRUE(PositionOf(nodes[0]).LessThan(PositionOf(nodes[1])));
742 EXPECT_TRUE(PositionOf(nodes[1]).LessThan(PositionOf(nodes[2])));
743 EXPECT_TRUE(PositionOf(nodes[2]).LessThan(PositionOf(nodes[3])));
744 EXPECT_TRUE(PositionOf(nodes[3]).LessThan(PositionOf(nodes[4])));
747 TEST_F(BookmarkModelObserverImplTest,
748 ShouldCallOnBookmarkModelBeingDeletedClosure) {
749 std::unique_ptr<SyncedBookmarkTracker> bookmark_tracker =
750 SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
752 NiceMock<base::MockCallback<base::OnceClosure>>
753 on_bookmark_model_being_deleted_closure_mock;
755 BookmarkModelObserverImpl observer(
756 /*nudge_for_commit_closure=*/base::DoNothing(),
757 on_bookmark_model_being_deleted_closure_mock.Get(),
758 bookmark_tracker.get());
760 EXPECT_CALL(on_bookmark_model_being_deleted_closure_mock, Run());
761 observer.BookmarkModelBeingDeleted(/*model=*/nullptr);
764 TEST_F(BookmarkModelObserverImplTest, ShouldNotIssueCommitUponFaviconLoad) {
765 const GURL kBookmarkUrl("http://www.url.com");
766 const GURL kIconUrl("http://www.url.com/favicon.ico");
767 const SkColor kColor = SK_ColorRED;
769 const bookmarks::BookmarkNode* bookmark_bar_node =
770 bookmark_model()->bookmark_bar_node();
771 const bookmarks::BookmarkNode* bookmark_node = bookmark_model()->AddURL(
772 /*parent=*/bookmark_bar_node, /*index=*/0, u"title", kBookmarkUrl);
774 ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
775 kBookmarkUrl, kIconUrl, CreateTestImage(kColor)));
776 SimulateCommitResponseForAllLocalChanges();
777 ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), IsEmpty());
779 const SyncedBookmarkTrackerEntity* entity =
780 bookmark_tracker()->GetEntityForBookmarkNode(bookmark_node);
781 ASSERT_THAT(entity, NotNull());
782 ASSERT_TRUE(entity->metadata().has_bookmark_favicon_hash());
783 const uint32_t initial_favicon_hash =
784 entity->metadata().bookmark_favicon_hash();
786 // Clear the specifics hash (as if the proto definition would have changed).
787 // This is needed because otherwise the commit is trivially optimized away
788 // (i.e. literally nothing changed).
789 bookmark_tracker()->ClearSpecificsHashForTest(entity);
791 // Mimic the very same favicon being loaded again (similar to a startup
792 // scenario). Note that OnFaviconsChanged() needs no icon URL to invalidate
793 // the favicon of a bookmark.
794 EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
795 bookmark_model()->OnFaviconsChanged(/*page_urls=*/{kBookmarkUrl},
796 /*icon_url=*/GURL());
797 ASSERT_TRUE(bookmark_node->is_favicon_loading());
798 ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
799 kBookmarkUrl, kIconUrl, CreateTestImage(kColor)));
801 EXPECT_TRUE(entity->metadata().has_bookmark_favicon_hash());
802 EXPECT_THAT(entity->metadata().bookmark_favicon_hash(),
803 Eq(initial_favicon_hash));
804 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), IsEmpty());
807 TEST_F(BookmarkModelObserverImplTest, ShouldCommitLocalFaviconChange) {
808 const GURL kBookmarkUrl("http://www.url.com");
809 const GURL kInitialIconUrl("http://www.url.com/initial.ico");
810 const GURL kFinalIconUrl("http://www.url.com/final.ico");
812 const bookmarks::BookmarkNode* bookmark_bar_node =
813 bookmark_model()->bookmark_bar_node();
814 const bookmarks::BookmarkNode* bookmark_node = bookmark_model()->AddURL(
815 /*parent=*/bookmark_bar_node, /*index=*/0, u"title", kBookmarkUrl);
817 ASSERT_TRUE(bookmark_node->is_favicon_loading());
818 ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
819 kBookmarkUrl, kInitialIconUrl, CreateTestImage(SK_ColorRED)));
820 SimulateCommitResponseForAllLocalChanges();
821 ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), IsEmpty());
823 const SyncedBookmarkTrackerEntity* entity =
824 bookmark_tracker()->GetEntityForBookmarkNode(bookmark_node);
825 ASSERT_THAT(entity, NotNull());
826 ASSERT_TRUE(entity->metadata().has_bookmark_favicon_hash());
827 const uint32_t initial_favicon_hash =
828 entity->metadata().bookmark_favicon_hash();
830 // A favicon change should trigger a commit nudge once the favicon loads, but
831 // not earlier. Note that OnFaviconsChanged() needs no icon URL to invalidate
832 // the favicon of a bookmark.
833 EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
834 bookmark_model()->OnFaviconsChanged(/*page_urls=*/{kBookmarkUrl},
835 /*icon_url=*/GURL());
836 ASSERT_TRUE(bookmark_node->is_favicon_loading());
838 EXPECT_CALL(*nudge_for_commit_closure(), Run());
839 ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
840 kBookmarkUrl, kFinalIconUrl, CreateTestImage(SK_ColorBLUE)));
842 EXPECT_TRUE(entity->metadata().has_bookmark_favicon_hash());
843 EXPECT_THAT(entity->metadata().bookmark_favicon_hash(),
844 Ne(initial_favicon_hash));
845 EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
846 ElementsAre(HasBookmarkNode(bookmark_node)));
849 TEST_F(BookmarkModelObserverImplTest,
850 ShouldNudgeForCommitOnFaviconLoadAfterRestart) {
851 const GURL kBookmarkUrl("http://www.url.com");
852 const GURL kIconUrl("http://www.url.com/favicon.ico");
853 const SkColor kColor = SK_ColorRED;
855 // Simulate work after restart. Add a new bookmark to a model and its
856 // specifics to the tracker without loading favicon.
857 bookmark_model()->RemoveObserver(observer());
859 // Add a new node with specifics and mark it unsynced.
860 const bookmarks::BookmarkNode* bookmark_bar_node =
861 bookmark_model()->bookmark_bar_node();
862 const bookmarks::BookmarkNode* bookmark_node = bookmark_model()->AddURL(
863 /*parent=*/bookmark_bar_node, /*index=*/0, u"title", kBookmarkUrl);
865 sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(
866 bookmark_node, bookmark_model(),
867 syncer::UniquePosition::InitialPosition(
868 syncer::UniquePosition::RandomSuffix())
870 /*force_favicon_load=*/false);
871 const gfx::Image favicon_image = CreateTestImage(kColor);
872 scoped_refptr<base::RefCountedMemory> favicon_bytes =
873 favicon_image.As1xPNGBytes();
874 specifics.mutable_bookmark()->set_favicon(favicon_bytes->front(),
875 favicon_bytes->size());
876 specifics.mutable_bookmark()->set_icon_url(kIconUrl.spec());
877 *specifics.mutable_bookmark()->mutable_unique_position() =
878 syncer::UniquePosition::InitialPosition(
879 syncer::UniquePosition::RandomSuffix())
882 const SyncedBookmarkTrackerEntity* entity = bookmark_tracker()->Add(
883 bookmark_node, "id", /*server_version=*/1, base::Time::Now(), specifics);
884 bookmark_tracker()->IncrementSequenceNumber(entity);
887 bookmark_model()->AddObserver(observer());
889 // Currently there is the unsynced |entity| which has no loaded favicon.
890 ASSERT_FALSE(bookmark_node->is_favicon_loaded());
891 ASSERT_TRUE(entity->IsUnsynced());
893 EXPECT_CALL(*nudge_for_commit_closure(), Run());
894 bookmark_model()->GetFavicon(bookmark_node);
895 ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
896 kBookmarkUrl, kIconUrl, CreateTestImage(SK_ColorRED)));
899 TEST_F(BookmarkModelObserverImplTest,
900 ShouldAddRestoredBookmarkWhenTombstoneCommitMayHaveStarted) {
901 const bookmarks::BookmarkNode* bookmark_bar_node =
902 bookmark_model()->bookmark_bar_node();
903 const bookmarks::BookmarkNode* folder =
904 bookmark_model()->AddFolder(bookmark_bar_node, 0, u"Title");
905 const syncer::ClientTagHash folder_client_tag_hash =
906 SyncedBookmarkTracker::GetClientTagHashFromGUID(folder->guid());
907 // Check that the bookmark was added by observer.
908 const SyncedBookmarkTrackerEntity* folder_entity =
909 bookmark_tracker()->GetEntityForBookmarkNode(folder);
910 ASSERT_THAT(folder_entity, NotNull());
911 ASSERT_TRUE(folder_entity->IsUnsynced());
912 SimulateCommitResponseForAllLocalChanges();
913 ASSERT_FALSE(folder_entity->IsUnsynced());
915 // Now delete the entity and restore it with the same bookmark node.
916 BookmarkUndoService undo_service;
917 undo_service.Start(bookmark_model());
918 bookmark_model()->Remove(folder);
920 // The removed bookmark must be saved in the undo service.
921 ASSERT_EQ(undo_service.undo_manager()->undo_count(), 1u);
922 ASSERT_THAT(bookmark_tracker()->GetEntityForBookmarkNode(folder), IsNull());
924 // Check that the entity is a tombstone now.
925 const std::vector<const SyncedBookmarkTrackerEntity*> local_changes =
926 bookmark_tracker()->GetEntitiesWithLocalChanges();
927 ASSERT_THAT(local_changes, ElementsAre(folder_entity));
928 ASSERT_TRUE(folder_entity->metadata().is_deleted());
930 bookmark_tracker()->GetEntityForClientTagHash(folder_client_tag_hash),
933 // Restore the removed bookmark.
934 undo_service.undo_manager()->Undo();
935 undo_service.Shutdown();
937 EXPECT_EQ(folder_entity,
938 bookmark_tracker()->GetEntityForBookmarkNode(folder));
940 bookmark_tracker()->GetEntityForClientTagHash(folder_client_tag_hash),
942 EXPECT_TRUE(folder_entity->IsUnsynced());
943 EXPECT_FALSE(folder_entity->metadata().is_deleted());
944 EXPECT_EQ(folder_entity->bookmark_node(), folder);
947 // Tests that the bookmark entity will be committed if its favicon is deleted.
948 TEST_F(BookmarkModelObserverImplTest, ShouldCommitOnDeleteFavicon) {
949 const GURL kBookmarkUrl("http://www.url.com");
950 const GURL kIconUrl("http://www.url.com/favicon.ico");
952 // Add a new node with specifics.
953 const bookmarks::BookmarkNode* bookmark_bar_node =
954 bookmark_model()->bookmark_bar_node();
955 const bookmarks::BookmarkNode* bookmark_node = bookmark_model()->AddURL(
956 /*parent=*/bookmark_bar_node, /*index=*/0, u"title", kBookmarkUrl);
958 ASSERT_TRUE(bookmark_node->is_favicon_loading());
959 ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
960 kBookmarkUrl, kIconUrl, CreateTestImage(SK_ColorRED)));
962 const SyncedBookmarkTrackerEntity* entity =
963 bookmark_tracker()->GetEntityForBookmarkNode(bookmark_node);
964 ASSERT_THAT(entity, NotNull());
965 ASSERT_TRUE(entity->IsUnsynced());
967 SimulateCommitResponseForAllLocalChanges();
969 ASSERT_FALSE(bookmark_tracker()->HasLocalChanges());
971 // Delete favicon and check that its deletion is committed.
972 bookmark_model()->OnFaviconsChanged({kBookmarkUrl}, GURL());
973 ASSERT_TRUE(bookmark_node->is_favicon_loading());
974 ASSERT_TRUE(bookmark_client()->SimulateEmptyFaviconLoaded(kBookmarkUrl));
976 EXPECT_TRUE(entity->IsUnsynced());
981 } // namespace sync_bookmarks