Upload upstream chromium 108.0.5359.1
[platform/framework/web/chromium-efl.git] / components / sync_bookmarks / bookmark_model_observer_impl_unittest.cc
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.
4
5 #include "components/sync_bookmarks/bookmark_model_observer_impl.h"
6
7 #include <algorithm>
8 #include <list>
9 #include <map>
10 #include <memory>
11 #include <utility>
12 #include <vector>
13
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"
35
36 namespace sync_bookmarks {
37
38 namespace {
39
40 using testing::ElementsAre;
41 using testing::Eq;
42 using testing::IsEmpty;
43 using testing::IsNull;
44 using testing::Ne;
45 using testing::NiceMock;
46 using testing::NotNull;
47 using testing::SizeIs;
48 using testing::UnorderedElementsAre;
49
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";
56
57 // Matches |arg| of type SyncedBookmarkTrackerEntity*.
58 MATCHER_P(HasBookmarkNode, node, "") {
59   return arg->bookmark_node() == node;
60 }
61
62 // Returns a single-color 16x16 image using |color|.
63 gfx::Image CreateTestImage(SkColor color) {
64   SkBitmap bitmap;
65   bitmap.allocN32Pixels(16, 16);
66   bitmap.eraseColor(color);
67   return gfx::Image::CreateFrom1xBitmap(bitmap);
68 }
69
70 class BookmarkModelObserverImplTest : public testing::Test {
71  public:
72   BookmarkModelObserverImplTest()
73       : bookmark_tracker_(
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(
82         kBookmarkBarTag);
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(
88         kOtherBookmarksTag);
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(
94         kMobileBookmarksTag);
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);
99   }
100
101   ~BookmarkModelObserverImplTest() override {
102     bookmark_model_->RemoveObserver(&observer_);
103   }
104
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(
111           entity, id,
112           /*server_version=*/1,
113           /*acked_sequence_number=*/entity->metadata().sequence_number());
114     }
115   }
116
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());
123   }
124
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";
129
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),
136           GURL(kUrl)));
137     }
138
139     // Verify number of entities local changes. Should be the same as number of
140     // new nodes.
141     DCHECK_EQ(bookmark_tracker()->GetEntitiesWithLocalChanges().size(),
142               num_bookmarks);
143
144     // All bookmarks should be tracked now (including permanent nodes).
145     DCHECK_EQ(bookmark_tracker()->TrackedEntitiesCountForTest(),
146               3 + num_bookmarks);
147
148     return nodes;
149   }
150
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_;
156   }
157   bookmarks::TestBookmarkClient* bookmark_client() {
158     return static_cast<bookmarks::TestBookmarkClient*>(
159         bookmark_model_->client());
160   }
161
162  private:
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_;
168 };
169
170 TEST_F(BookmarkModelObserverImplTest,
171        BookmarkAddedShouldPutInTheTrackerAndNudgeForCommit) {
172   const std::string kTitle = "title";
173   const std::string kUrl = "http://www.url.com";
174
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),
180       GURL(kUrl));
181
182   EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 4U);
183
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()));
190 }
191
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";
200
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),
205       GURL(kUrl1));
206   const bookmarks::BookmarkNode* bookmark_node2 = bookmark_model()->AddURL(
207       /*parent=*/bookmark_bar_node, /*index=*/0, base::UTF8ToUTF16(kTitle2),
208       GURL(kUrl2));
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);
213
214   SimulateCommitResponseForAllLocalChanges();
215
216   // There should be no local changes now.
217   ASSERT_TRUE(bookmark_tracker()->GetEntitiesWithLocalChanges().empty());
218
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)));
226
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);
231
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)));
236
237   // Now update metainfo of the 1st node.
238   EXPECT_CALL(*nudge_for_commit_closure(), Run());
239   bookmark_model()->SetNodeMetaInfo(bookmark_node1, "key", "value");
240 }
241
242 TEST_F(BookmarkModelObserverImplTest,
243        BookmarkMovedShouldUpdateTheTrackerAndNudgeForCommit) {
244   // Build this structure:
245   // bookmark_bar
246   //  |- folder1
247   //      |- bookmark1
248   const GURL kUrl("http://www.url1.com");
249
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);
256
257   // Verify number of entities local changes. Should be the same as number of
258   // new nodes.
259   ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 2U);
260
261   // All bookmarks should be tracked now.
262   ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 5U);
263
264   SimulateCommitResponseForAllLocalChanges();
265
266   // There should be no local changes now.
267   ASSERT_TRUE(bookmark_tracker()->GetEntitiesWithLocalChanges().empty());
268
269   // Now change it to this structure.
270   // Build this structure:
271   // bookmark_bar
272   //  |- bookmark1
273   //  |- folder1
274
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)));
278 }
279
280 TEST_F(BookmarkModelObserverImplTest,
281        ReorderChildrenShouldUpdateTheTrackerAndNudgeForCommit) {
282   std::vector<const bookmarks::BookmarkNode*> nodes =
283       GenerateBookmarkNodes(/*num_bookmarks=*/4);
284
285   SimulateCommitResponseForAllLocalChanges();
286
287   // Reorder it to be (2 bookmarks have been moved):
288   // bookmark_bar
289   //  |- node1
290   //  |- node3
291   //  |- node0
292   //  |- node2
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])));
298
299   // Only 2 moved nodes should have local changes to commit.
300   EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), SizeIs(2));
301 }
302
303 TEST_F(BookmarkModelObserverImplTest,
304        ShouldReorderChildrenAndUpdateOnlyMovedToRightBookmark) {
305   std::vector<const bookmarks::BookmarkNode*> nodes =
306       GenerateBookmarkNodes(/*num_bookmarks=*/4);
307
308   SimulateCommitResponseForAllLocalChanges();
309
310   // Reorder it to be:
311   // bookmark_bar
312   //  |- node1
313   //  |- node2
314   //  |- node0 (moved)
315   //  |- node3
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])));
321
322   // Only one moved node should have local changes to commit.
323   EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
324               UnorderedElementsAre(HasBookmarkNode(nodes[0])));
325 }
326
327 TEST_F(BookmarkModelObserverImplTest,
328        ShouldReorderChildrenAndUpdateOnlyMovedToLeftBookmark) {
329   std::vector<const bookmarks::BookmarkNode*> nodes =
330       GenerateBookmarkNodes(/*num_bookmarks=*/4);
331
332   SimulateCommitResponseForAllLocalChanges();
333
334   // Reorder it to be:
335   // bookmark_bar
336   //  |- node0
337   //  |- node3 (moved)
338   //  |- node1
339   //  |- node2
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])));
345
346   // Only one moved node should have local changes to commit.
347   EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
348               UnorderedElementsAre(HasBookmarkNode(nodes[3])));
349 }
350
351 TEST_F(BookmarkModelObserverImplTest,
352        ShouldReorderWhenBookmarkMovedToLastPosition) {
353   std::vector<const bookmarks::BookmarkNode*> nodes =
354       GenerateBookmarkNodes(/*num_bookmarks=*/4);
355
356   SimulateCommitResponseForAllLocalChanges();
357
358   // Reorder it to be:
359   // bookmark_bar
360   //  |- node1
361   //  |- node2
362   //  |- node3
363   //  |- node0 (moved)
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])));
369
370   // Only one moved node should have local changes to commit.
371   EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
372               UnorderedElementsAre(HasBookmarkNode(nodes[0])));
373 }
374
375 TEST_F(BookmarkModelObserverImplTest,
376        ShouldReorderWhenBookmarkMovedToFirstPosition) {
377   std::vector<const bookmarks::BookmarkNode*> nodes =
378       GenerateBookmarkNodes(/*num_bookmarks=*/4);
379
380   SimulateCommitResponseForAllLocalChanges();
381
382   // Reorder it to be:
383   // bookmark_bar
384   //  |- node3 (moved)
385   //  |- node0
386   //  |- node1
387   //  |- node2
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])));
393
394   // Only one moved node should have local changes to commit.
395   EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(),
396               UnorderedElementsAre(HasBookmarkNode(nodes[3])));
397 }
398
399 TEST_F(BookmarkModelObserverImplTest, ShouldReorderWhenAllBookmarksReversed) {
400   // In this case almost all the bookmarks should be updated apart from only one
401   // bookmark.
402   std::vector<const bookmarks::BookmarkNode*> nodes =
403       GenerateBookmarkNodes(/*num_bookmarks=*/4);
404
405   SimulateCommitResponseForAllLocalChanges();
406
407   // Reorder it to be (all nodes are moved):
408   // bookmark_bar
409   //  |- node3
410   //  |- node2
411   //  |- node1
412   //  |- node0
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])));
418
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));
423 }
424
425 TEST_F(BookmarkModelObserverImplTest,
426        ShouldNotReorderIfAllBookmarksStillOrdered) {
427   std::vector<const bookmarks::BookmarkNode*> nodes =
428       GenerateBookmarkNodes(/*num_bookmarks=*/4);
429
430   SimulateCommitResponseForAllLocalChanges();
431
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])));
438
439   // The bookmarks remain in the same order, nothing to commit.
440   EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), IsEmpty());
441 }
442
443 TEST_F(BookmarkModelObserverImplTest,
444        BookmarkRemovalShouldUpdateTheTrackerAndNudgeForCommit) {
445   // Build this structure:
446   // bookmark_bar
447   //  |- folder1
448   //      |- bookmark1
449   //      |- folder2
450   //          |- bookmark2
451   //          |- bookmark3
452
453   // and then delete folder2.
454   const GURL kUrl("http://www.url1.com");
455
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);
468
469   // All bookmarks should be tracked now.
470   ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 8U);
471
472   SimulateCommitResponseForAllLocalChanges();
473
474   // There should be no local changes now.
475   ASSERT_TRUE(bookmark_tracker()->GetEntitiesWithLocalChanges().empty());
476
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);
483
484   ASSERT_FALSE(folder2_entity->metadata().is_deleted());
485   ASSERT_FALSE(bookmark2_entity->metadata().is_deleted());
486   ASSERT_FALSE(bookmark3_entity->metadata().is_deleted());
487
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();
493   // Delete folder2.
494   EXPECT_CALL(*nudge_for_commit_closure(), Run());
495   bookmark_model()->Remove(folder2_node);
496
497   // folder2, bookmark2, and bookmark3 should be marked deleted.
498   EXPECT_TRUE(bookmark_tracker()
499                   ->GetEntityForSyncId(folder2_entity_id)
500                   ->metadata()
501                   .is_deleted());
502   EXPECT_TRUE(bookmark_tracker()
503                   ->GetEntityForSyncId(bookmark2_entity_id)
504                   ->metadata()
505                   .is_deleted());
506   EXPECT_TRUE(bookmark_tracker()
507                   ->GetEntityForSyncId(bookmark3_entity_id)
508                   ->metadata()
509                   .is_deleted());
510
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
513   // deletions).
514   EXPECT_THAT(
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)));
519
520   // folder1 and bookmark1 are still tracked.
521   EXPECT_TRUE(bookmark_tracker()->GetEntityForBookmarkNode(folder1_node));
522   EXPECT_TRUE(bookmark_tracker()->GetEntityForBookmarkNode(bookmark1_node));
523 }
524
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");
531
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);
538
539   bookmark_tracker()->MarkCommitMayHaveStarted(entity);
540
541   // Remove the folder.
542   bookmark_model()->Remove(folder_node);
543
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);
549
550   // There should still be one local change (the deletion).
551   EXPECT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 1U);
552
553   // Entity is still tracked.
554   EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 4U);
555
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);
562 }
563
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");
570
571   // Node should be tracked now.
572   ASSERT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 4U);
573   const std::string id = bookmark_tracker()
574                              ->GetEntityForBookmarkNode(folder_node)
575                              ->metadata()
576                              .server_id();
577   ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges().size(), 1U);
578
579   // Remove the folder.
580   bookmark_model()->Remove(folder_node);
581
582   // Entity should have been dropped.
583   EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), 3U);
584 }
585
586 TEST_F(BookmarkModelObserverImplTest, ShouldPositionSiblings) {
587   const std::string kTitle = "title";
588   const std::string kUrl = "http://www.url.com";
589
590   // Build this structure:
591   // bookmark_bar
592   //  |- node1
593   //  |- node2
594   // Expectation:
595   //  p1 < p2
596
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),
601       GURL(kUrl));
602
603   const bookmarks::BookmarkNode* bookmark_node2 = bookmark_model()->AddURL(
604       /*parent=*/bookmark_bar_node, /*index=*/1, base::UTF8ToUTF16(kTitle),
605       GURL(kUrl));
606
607   EXPECT_TRUE(PositionOf(bookmark_node1).LessThan(PositionOf(bookmark_node2)));
608
609   // Now insert node3 at index 1 to build this structure:
610   // bookmark_bar
611   //  |- node1
612   //  |- node3
613   //  |- node2
614   // Expectation:
615   //  p1 < p2 (still holds)
616   //  p1 < p3
617   //  p3 < p2
618
619   const bookmarks::BookmarkNode* bookmark_node3 = bookmark_model()->AddURL(
620       /*parent=*/bookmark_bar_node, /*index=*/1, base::UTF8ToUTF16(kTitle),
621       GURL(kUrl));
622   EXPECT_THAT(bookmark_tracker()->TrackedEntitiesCountForTest(), Eq(6U));
623
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)));
627 }
628
629 TEST_F(BookmarkModelObserverImplTest, ShouldNotSyncUnsyncableBookmarks) {
630   auto client = std::make_unique<bookmarks::TestBookmarkClient>();
631   bookmarks::BookmarkNode* managed_node = client->EnableManagedNode();
632
633   std::unique_ptr<bookmarks::BookmarkModel> model =
634       bookmarks::TestBookmarkClient::CreateModelWithClient(std::move(client));
635
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())
643           .ToProto();
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(
649       kOtherBookmarksTag);
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());
664
665   model->AddObserver(&observer);
666
667   EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
668   // In the TestBookmarkClient, descendants of managed nodes shouldn't be
669   // synced.
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);
675
676   EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
677   // In the TestBookmarkClient, descendants of managed nodes shouldn't be
678   // synced.
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);
683
684   EXPECT_CALL(*nudge_for_commit_closure(), Run()).Times(0);
685   // In the TestBookmarkClient, descendants of managed nodes shouldn't be
686   // synced.
687   model->Remove(unsyncable_node);
688
689   // Only permanent folders should be tracked.
690   EXPECT_THAT(bookmark_tracker->TrackedEntitiesCountForTest(), 3U);
691   model->RemoveObserver(&observer);
692 }
693
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())
709           .ToProto();
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);
714
715   // Build this structure:
716   // bookmark_bar
717   //  |- folder0
718   //  |- folder1
719   //  |- folder2
720   //  |- folder3
721   //  |- folder4
722
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)));
728   }
729
730   // Now simulate calling the observer as if the nodes are added in that order.
731   // 4,0,2,3,1.
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);
737
738   ASSERT_THAT(bookmark_tracker->TrackedEntitiesCountForTest(), 6U);
739
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])));
745 }
746
747 TEST_F(BookmarkModelObserverImplTest,
748        ShouldCallOnBookmarkModelBeingDeletedClosure) {
749   std::unique_ptr<SyncedBookmarkTracker> bookmark_tracker =
750       SyncedBookmarkTracker::CreateEmpty(sync_pb::ModelTypeState());
751
752   NiceMock<base::MockCallback<base::OnceClosure>>
753       on_bookmark_model_being_deleted_closure_mock;
754
755   BookmarkModelObserverImpl observer(
756       /*nudge_for_commit_closure=*/base::DoNothing(),
757       on_bookmark_model_being_deleted_closure_mock.Get(),
758       bookmark_tracker.get());
759
760   EXPECT_CALL(on_bookmark_model_being_deleted_closure_mock, Run());
761   observer.BookmarkModelBeingDeleted(/*model=*/nullptr);
762 }
763
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;
768
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);
773
774   ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
775       kBookmarkUrl, kIconUrl, CreateTestImage(kColor)));
776   SimulateCommitResponseForAllLocalChanges();
777   ASSERT_THAT(bookmark_tracker()->GetEntitiesWithLocalChanges(), IsEmpty());
778
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();
785
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);
790
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)));
800
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());
805 }
806
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");
811
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);
816
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());
822
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();
829
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());
837
838   EXPECT_CALL(*nudge_for_commit_closure(), Run());
839   ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
840       kBookmarkUrl, kFinalIconUrl, CreateTestImage(SK_ColorBLUE)));
841
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)));
847 }
848
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;
854
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());
858
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);
864
865   sync_pb::EntitySpecifics specifics = CreateSpecificsFromBookmarkNode(
866       bookmark_node, bookmark_model(),
867       syncer::UniquePosition::InitialPosition(
868           syncer::UniquePosition::RandomSuffix())
869           .ToProto(),
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())
880           .ToProto();
881
882   const SyncedBookmarkTrackerEntity* entity = bookmark_tracker()->Add(
883       bookmark_node, "id", /*server_version=*/1, base::Time::Now(), specifics);
884   bookmark_tracker()->IncrementSequenceNumber(entity);
885
886   // Restore state.
887   bookmark_model()->AddObserver(observer());
888
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());
892
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)));
897 }
898
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());
914
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);
919
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());
923
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());
929   ASSERT_EQ(
930       bookmark_tracker()->GetEntityForClientTagHash(folder_client_tag_hash),
931       folder_entity);
932
933   // Restore the removed bookmark.
934   undo_service.undo_manager()->Undo();
935   undo_service.Shutdown();
936
937   EXPECT_EQ(folder_entity,
938             bookmark_tracker()->GetEntityForBookmarkNode(folder));
939   EXPECT_EQ(
940       bookmark_tracker()->GetEntityForClientTagHash(folder_client_tag_hash),
941       folder_entity);
942   EXPECT_TRUE(folder_entity->IsUnsynced());
943   EXPECT_FALSE(folder_entity->metadata().is_deleted());
944   EXPECT_EQ(folder_entity->bookmark_node(), folder);
945 }
946
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");
951
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);
957
958   ASSERT_TRUE(bookmark_node->is_favicon_loading());
959   ASSERT_TRUE(bookmark_client()->SimulateFaviconLoaded(
960       kBookmarkUrl, kIconUrl, CreateTestImage(SK_ColorRED)));
961
962   const SyncedBookmarkTrackerEntity* entity =
963       bookmark_tracker()->GetEntityForBookmarkNode(bookmark_node);
964   ASSERT_THAT(entity, NotNull());
965   ASSERT_TRUE(entity->IsUnsynced());
966
967   SimulateCommitResponseForAllLocalChanges();
968
969   ASSERT_FALSE(bookmark_tracker()->HasLocalChanges());
970
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));
975
976   EXPECT_TRUE(entity->IsUnsynced());
977 }
978
979 }  // namespace
980
981 }  // namespace sync_bookmarks