summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/sync/test_bookmark_structure_changes.js')
-rw-r--r--toolkit/components/places/tests/sync/test_bookmark_structure_changes.js2870
1 files changed, 2870 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js b/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
new file mode 100644
index 0000000000..fd4f3fdb6e
--- /dev/null
+++ b/toolkit/components/places/tests/sync/test_bookmark_structure_changes.js
@@ -0,0 +1,2870 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_value_structure_conflict() {
+ let buf = await openMirror("value_structure_conflict");
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ },
+ ],
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ url: "http://example.com/e",
+ title: "E",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA", "folderDDDDDD"],
+ modified: Date.now() / 1000 - 60,
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkBBBB", "bookmarkCCCC"],
+ modified: Date.now() / 1000 - 60,
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ modified: Date.now() / 1000 - 60,
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ modified: Date.now() / 1000 - 60,
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkEEEE"],
+ modified: Date.now() / 1000 - 60,
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ modified: Date.now() / 1000 - 60,
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local value change");
+ await PlacesUtils.bookmarks.update({
+ guid: "folderAAAAAA",
+ title: "A (local)",
+ });
+
+ info("Make local structure change");
+ await PlacesUtils.bookmarks.update({
+ guid: "bookmarkBBBB",
+ parentGuid: "folderDDDDDD",
+ index: 0,
+ });
+
+ info("Make remote value change");
+ await storeRecords(buf, [
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D (remote)",
+ children: ["bookmarkEEEE"],
+ modified: Date.now() / 1000 + 60,
+ },
+ ]);
+
+ info("Apply remote");
+ let observer = expectBookmarkChangeNotifications();
+ let changesToUpload = await buf.apply({
+ remoteTimeSeconds: Date.now() / 1000,
+ notifyInStableOrder: true,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["folderDDDDDD"],
+ "Should leave D with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkBBBB", "folderAAAAAA", "folderDDDDDD"],
+ deleted: [],
+ },
+ "Should upload records for merged and new local items"
+ );
+
+ let localItemIds = await PlacesUtils.promiseManyItemIds([
+ "folderAAAAAA",
+ "bookmarkEEEE",
+ "bookmarkBBBB",
+ "folderDDDDDD",
+ ]);
+ observer.check([
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("bookmarkEEEE"),
+ oldIndex: 1,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "bookmarkEEEE",
+ oldParentGuid: "folderDDDDDD",
+ newParentGuid: "folderDDDDDD",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "http://example.com/e",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("bookmarkBBBB"),
+ oldIndex: 0,
+ newIndex: 1,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "bookmarkBBBB",
+ oldParentGuid: "folderDDDDDD",
+ newParentGuid: "folderDDDDDD",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "http://example.com/b",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-title-changed",
+ params: {
+ itemId: localItemIds.get("folderDDDDDD"),
+ title: "D (remote)",
+ guid: "folderDDDDDD",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ },
+ },
+ ]);
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A (local)",
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "D (remote)",
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ ],
+ },
+ "Should reconcile structure and value changes"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_move() {
+ let buf = await openMirror("move");
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "devFolder___",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "Dev",
+ children: [
+ {
+ guid: "mdnBmk______",
+ title: "MDN",
+ url: "https://developer.mozilla.org",
+ },
+ {
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ guid: "mozFolder___",
+ title: "Mozilla",
+ children: [
+ {
+ guid: "fxBmk_______",
+ title: "Get Firefox!",
+ url: "http://getfirefox.com/",
+ },
+ {
+ guid: "nightlyBmk__",
+ title: "Nightly",
+ url: "https://nightly.mozilla.org",
+ },
+ ],
+ },
+ {
+ guid: "wmBmk_______",
+ title: "Webmaker",
+ url: "https://webmaker.org",
+ },
+ ],
+ },
+ {
+ guid: "bzBmk_______",
+ title: "Bugzilla",
+ url: "https://bugzilla.mozilla.org",
+ },
+ ],
+ });
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "unfiled",
+ parentid: "places",
+ type: "folder",
+ children: ["mozFolder___"],
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["devFolder___"],
+ },
+ {
+ // Moving to toolbar.
+ id: "devFolder___",
+ parentid: "toolbar",
+ type: "folder",
+ title: "Dev",
+ children: ["bzBmk_______", "wmBmk_______"],
+ },
+ {
+ // Moving to "Mozilla".
+ id: "mdnBmk______",
+ parentid: "mozFolder___",
+ type: "bookmark",
+ title: "MDN",
+ bmkUri: "https://developer.mozilla.org",
+ },
+ {
+ // Rearranging children and moving to unfiled.
+ id: "mozFolder___",
+ parentid: "unfiled",
+ type: "folder",
+ title: "Mozilla",
+ children: ["nightlyBmk__", "mdnBmk______", "fxBmk_______"],
+ },
+ {
+ id: "fxBmk_______",
+ parentid: "mozFolder___",
+ type: "bookmark",
+ title: "Get Firefox!",
+ bmkUri: "http://getfirefox.com/",
+ },
+ {
+ id: "nightlyBmk__",
+ parentid: "mozFolder___",
+ type: "bookmark",
+ title: "Nightly",
+ bmkUri: "https://nightly.mozilla.org",
+ },
+ {
+ id: "wmBmk_______",
+ parentid: "devFolder___",
+ type: "bookmark",
+ title: "Webmaker",
+ bmkUri: "https://webmaker.org",
+ },
+ {
+ id: "bzBmk_______",
+ parentid: "devFolder___",
+ type: "bookmark",
+ title: "Bugzilla",
+ bmkUri: "https://bugzilla.mozilla.org",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let observer = expectBookmarkChangeNotifications();
+ let changesToUpload = await buf.apply({
+ notifyInStableOrder: true,
+ });
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [],
+ deleted: [],
+ },
+ "Should not upload records for remotely moved items"
+ );
+
+ let localItemIds = await PlacesUtils.promiseManyItemIds([
+ "devFolder___",
+ "mozFolder___",
+ "bzBmk_______",
+ "wmBmk_______",
+ "nightlyBmk__",
+ "mdnBmk______",
+ "fxBmk_______",
+ ]);
+ observer.check([
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("devFolder___"),
+ oldIndex: 0,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ guid: "devFolder___",
+ oldParentGuid: PlacesUtils.bookmarks.menuGuid,
+ newParentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("mozFolder___"),
+ oldIndex: 1,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ guid: "mozFolder___",
+ oldParentGuid: "devFolder___",
+ newParentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("bzBmk_______"),
+ oldIndex: 1,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "bzBmk_______",
+ oldParentGuid: PlacesUtils.bookmarks.menuGuid,
+ newParentGuid: "devFolder___",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "https://bugzilla.mozilla.org/",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("wmBmk_______"),
+ oldIndex: 2,
+ newIndex: 1,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "wmBmk_______",
+ oldParentGuid: "devFolder___",
+ newParentGuid: "devFolder___",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "https://webmaker.org/",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("nightlyBmk__"),
+ oldIndex: 1,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "nightlyBmk__",
+ oldParentGuid: "mozFolder___",
+ newParentGuid: "mozFolder___",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "https://nightly.mozilla.org/",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("mdnBmk______"),
+ oldIndex: 0,
+ newIndex: 1,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "mdnBmk______",
+ oldParentGuid: "devFolder___",
+ newParentGuid: "mozFolder___",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "https://developer.mozilla.org/",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("fxBmk_______"),
+ oldIndex: 0,
+ newIndex: 2,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "fxBmk_______",
+ oldParentGuid: "mozFolder___",
+ newParentGuid: "mozFolder___",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "http://getfirefox.com/",
+ isTagging: false,
+ },
+ },
+ ]);
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "devFolder___",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "Dev",
+ children: [
+ {
+ guid: "bzBmk_______",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "Bugzilla",
+ url: "https://bugzilla.mozilla.org/",
+ },
+ {
+ guid: "wmBmk_______",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "Webmaker",
+ url: "https://webmaker.org/",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ children: [
+ {
+ guid: "mozFolder___",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "Mozilla",
+ children: [
+ {
+ guid: "nightlyBmk__",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "Nightly",
+ url: "https://nightly.mozilla.org/",
+ },
+ {
+ guid: "mdnBmk______",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "MDN",
+ url: "https://developer.mozilla.org/",
+ },
+ {
+ guid: "fxBmk_______",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "Get Firefox!",
+ url: "http://getfirefox.com/",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should move and reorder bookmarks to match remote"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_move_into_parent_sibling() {
+ // This test moves a bookmark that exists locally into a new folder that only
+ // exists remotely, and is a later sibling of the local parent. This ensures
+ // we set up the local structure before applying structure changes.
+ let buf = await openMirror("move_into_parent_sibling");
+
+ info("Set up mirror: Menu > A > B");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make remote changes: Menu > (A (B > C))");
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA", "folderCCCCCC"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "menu",
+ type: "folder",
+ title: "C",
+ children: ["bookmarkBBBB"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderCCCCCC",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ ]);
+
+ info("Apply remote");
+ let observer = expectBookmarkChangeNotifications();
+ let changesToUpload = await buf.apply({
+ notifyInStableOrder: true,
+ });
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: [],
+ deleted: [],
+ },
+ "Should not upload records for remote-only structure changes"
+ );
+
+ let localItemIds = await PlacesUtils.promiseManyItemIds([
+ "folderCCCCCC",
+ "bookmarkBBBB",
+ "folderAAAAAA",
+ PlacesUtils.bookmarks.menuGuid,
+ ]);
+ observer.check([
+ {
+ name: "bookmark-added",
+ params: {
+ itemId: localItemIds.get("folderCCCCCC"),
+ parentId: localItemIds.get(PlacesUtils.bookmarks.menuGuid),
+ index: 1,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ urlHref: "",
+ title: "C",
+ guid: "folderCCCCCC",
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("bookmarkBBBB"),
+ oldIndex: 0,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "bookmarkBBBB",
+ oldParentGuid: "folderAAAAAA",
+ newParentGuid: "folderCCCCCC",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "http://example.com/b",
+ isTagging: false,
+ },
+ },
+ ]);
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.menuGuid,
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ },
+ {
+ guid: "folderCCCCCC",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "C",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ ],
+ },
+ ],
+ },
+ "Should set up local structure correctly"
+ );
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_complex_move_with_additions() {
+ let mergeTelemetryCounts;
+ let buf = await openMirror("complex_move_with_additions", {
+ recordStepTelemetry(name, took, counts) {
+ if (name == "merge") {
+ mergeTelemetryCounts = counts;
+ }
+ },
+ });
+
+ info("Set up mirror: Menu > A > (B C)");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ },
+ ],
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkBBBB", "bookmarkCCCC"],
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info("Make local change: Menu > A > (B C D)");
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkDDDD",
+ parentGuid: "folderAAAAAA",
+ title: "D (local)",
+ url: "http://example.com/d-local",
+ });
+
+ info("Make remote change: ((Menu > C) (Toolbar > A > (B E)))");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkCCCC"],
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA"],
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "toolbar",
+ type: "folder",
+ title: "A",
+ children: ["bookmarkBBBB", "bookmarkEEEE"],
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "menu",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "folderAAAAAA",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let observer = expectBookmarkChangeNotifications();
+ let changesToUpload = await buf.apply({
+ notifyInStableOrder: true,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["folderAAAAAA"],
+ "Should leave A with new remote structure unmerged"
+ );
+ deepEqual(
+ mergeTelemetryCounts,
+ [{ name: "items", count: 10 }],
+ "Should record telemetry with structure change counts"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkDDDD", "folderAAAAAA"],
+ deleted: [],
+ },
+ "Should upload new records for (A D)"
+ );
+
+ let localItemIds = await PlacesUtils.promiseManyItemIds([
+ "bookmarkEEEE",
+ "folderAAAAAA",
+ "bookmarkCCCC",
+ ]);
+ observer.check([
+ {
+ name: "bookmark-added",
+ params: {
+ itemId: localItemIds.get("bookmarkEEEE"),
+ parentId: localItemIds.get("folderAAAAAA"),
+ index: 1,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ urlHref: "http://example.com/e",
+ title: "E",
+ guid: "bookmarkEEEE",
+ parentGuid: "folderAAAAAA",
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("bookmarkCCCC"),
+ oldIndex: 1,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ guid: "bookmarkCCCC",
+ oldParentGuid: "folderAAAAAA",
+ newParentGuid: PlacesUtils.bookmarks.menuGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "http://example.com/c",
+ isTagging: false,
+ },
+ },
+ {
+ name: "bookmark-moved",
+ params: {
+ itemId: localItemIds.get("folderAAAAAA"),
+ oldIndex: 0,
+ newIndex: 0,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ guid: "folderAAAAAA",
+ oldParentGuid: PlacesUtils.bookmarks.menuGuid,
+ newParentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ urlHref: "",
+ isTagging: false,
+ },
+ },
+ ]);
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ // We can guarantee child order (B E D), since we always walk remote
+ // children first, and the remote folder A record is newer than the
+ // local folder. If the local folder were newer, the order would be
+ // (D B E).
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "A",
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "D (local)",
+ url: "http://example.com/d-local",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should take remote order and preserve local children"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_reorder_and_insert() {
+ let buf = await openMirror("reorder_and_insert");
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ url: "http://example.com/a",
+ title: "A",
+ },
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ url: "http://example.com/d",
+ title: "D",
+ },
+ {
+ guid: "bookmarkEEEE",
+ url: "http://example.com/e",
+ title: "E",
+ },
+ {
+ guid: "bookmarkFFFF",
+ url: "http://example.com/f",
+ title: "F",
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "bookmarkBBBB", "bookmarkCCCC"],
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "menu",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkDDDD", "bookmarkEEEE", "bookmarkFFFF"],
+ },
+ {
+ id: "bookmarkDDDD",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "D",
+ bmkUri: "http://example.com/d",
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ },
+ {
+ id: "bookmarkFFFF",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "F",
+ bmkUri: "http://example.com/f",
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ let now = Date.now();
+
+ info("Make local changes: Reorder Menu, Toolbar > (G H)");
+ await PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.menuGuid, [
+ "bookmarkCCCC",
+ "bookmarkAAAA",
+ "bookmarkBBBB",
+ ]);
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ guid: "bookmarkGGGG",
+ url: "http://example.com/g",
+ title: "G",
+ dateAdded: new Date(now),
+ lastModified: new Date(now),
+ },
+ {
+ guid: "bookmarkHHHH",
+ url: "http://example.com/h",
+ title: "H",
+ dateAdded: new Date(now),
+ lastModified: new Date(now),
+ },
+ ],
+ });
+
+ info("Make remote changes: Reorder Toolbar, Menu > (I J)");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ // The server has a newer toolbar, so we should use the remote order (F D E)
+ // as the base, then append (G H).
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkFFFF", "bookmarkDDDD", "bookmarkEEEE"],
+ modified: now / 1000 + 5,
+ },
+ {
+ // The server has an older menu, so we should use the local order (C A B)
+ // as the base, then append (I J).
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: [
+ "bookmarkAAAA",
+ "bookmarkBBBB",
+ "bookmarkCCCC",
+ "bookmarkIIII",
+ "bookmarkJJJJ",
+ ],
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkIIII",
+ parentid: "menu",
+ type: "bookmark",
+ title: "I",
+ bmkUri: "http://example.com/i",
+ },
+ {
+ id: "bookmarkJJJJ",
+ parentid: "menu",
+ type: "bookmark",
+ title: "J",
+ bmkUri: "http://example.com/j",
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ remoteTimeSeconds: now / 1000,
+ localTimeSeconds: now / 1000,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [PlacesUtils.bookmarks.menuGuid, PlacesUtils.bookmarks.toolbarGuid],
+ "Should leave roots with new remote structure unmerged"
+ );
+
+ let idsToUpload = inspectChangeRecords(changesToUpload);
+ deepEqual(
+ idsToUpload,
+ {
+ updated: ["bookmarkGGGG", "bookmarkHHHH", "menu", "toolbar"],
+ deleted: [],
+ },
+ "Should upload records for merged and new local items"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ url: "http://example.com/c",
+ title: "C",
+ },
+ {
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ url: "http://example.com/a",
+ title: "A",
+ },
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ url: "http://example.com/b",
+ title: "B",
+ },
+ {
+ guid: "bookmarkIIII",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 3,
+ url: "http://example.com/i",
+ title: "I",
+ },
+ {
+ guid: "bookmarkJJJJ",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 4,
+ url: "http://example.com/j",
+ title: "J",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ url: "http://example.com/f",
+ title: "F",
+ },
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ url: "http://example.com/d",
+ title: "D",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ url: "http://example.com/e",
+ title: "E",
+ },
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 3,
+ url: "http://example.com/g",
+ title: "G",
+ },
+ {
+ guid: "bookmarkHHHH",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 4,
+ url: "http://example.com/h",
+ title: "H",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should use timestamps to decide base folder order"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_newer_remote_moves() {
+ let now = Date.now();
+ let buf = await openMirror("newer_remote_moves");
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ url: "http://example.com/a",
+ title: "A",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "B",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ url: "http://example.com/e",
+ title: "E",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ {
+ guid: "folderFFFFFF",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "F",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ children: [
+ {
+ guid: "bookmarkGGGG",
+ url: "http://example.com/g",
+ title: "G",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ },
+ {
+ guid: "folderHHHHHH",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "H",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "folderBBBBBB", "folderDDDDDD"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "menu",
+ type: "folder",
+ title: "B",
+ children: ["bookmarkCCCC"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkEEEE", "folderFFFFFF", "folderHHHHHH"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderFFFFFF",
+ parentid: "toolbar",
+ type: "folder",
+ title: "F",
+ children: ["bookmarkGGGG"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkGGGG",
+ parentid: "folderFFFFFF",
+ type: "bookmark",
+ title: "G",
+ bmkUri: "http://example.com/g",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderHHHHHH",
+ parentid: "toolbar",
+ type: "folder",
+ title: "H",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info(
+ "Make local changes: Unfiled > A, Mobile > B; Toolbar > (H F E); D > C; H > G"
+ );
+ let localMoves = [
+ {
+ guid: "bookmarkAAAA",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ },
+ {
+ guid: "folderBBBBBB",
+ parentGuid: PlacesUtils.bookmarks.mobileGuid,
+ },
+ {
+ guid: "bookmarkCCCC",
+ parentGuid: "folderDDDDDD",
+ },
+ {
+ guid: "bookmarkGGGG",
+ parentGuid: "folderHHHHHH",
+ },
+ ];
+ for (let { guid, parentGuid } of localMoves) {
+ await PlacesUtils.bookmarks.update({
+ guid,
+ parentGuid,
+ index: 0,
+ lastModified: new Date(now - 2500),
+ });
+ }
+ await PlacesUtils.bookmarks.reorder(
+ PlacesUtils.bookmarks.toolbarGuid,
+ ["folderHHHHHH", "folderFFFFFF", "bookmarkEEEE"],
+ { lastModified: new Date(now - 2500) }
+ );
+
+ info(
+ "Make remote changes: Mobile > A, Unfiled > B; Toolbar > (F E H); D > G; H > C"
+ );
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderDDDDDD"],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ id: "mobile",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA"],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ // This is similar to H > C, explained below, except we'll always reupload
+ // the mobile root, because we always prefer the local state for roots.
+ id: "bookmarkAAAA",
+ parentid: "mobile",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ id: "unfiled",
+ parentid: "places",
+ type: "folder",
+ children: ["folderBBBBBB"],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "unfiled",
+ type: "folder",
+ title: "B",
+ children: [],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["folderFFFFFF", "bookmarkEEEE", "folderHHHHHH"],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ id: "folderHHHHHH",
+ parentid: "toolbar",
+ type: "folder",
+ title: "H",
+ children: ["bookmarkCCCC"],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ // Reparenting an item uploads records for the item and its parent.
+ // The merger would still work if we only marked H as unmerged; we'd
+ // then use the remote state for H, and local state for C. Since C was
+ // changed locally, we'll reupload it, even though it didn't actually
+ // change.
+ id: "bookmarkCCCC",
+ parentid: "folderHHHHHH",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkGGGG"],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ children: ["bookmarkGGGG"],
+ },
+ {
+ id: "folderFFFFFF",
+ parentid: "toolbar",
+ type: "folder",
+ title: "F",
+ children: [],
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ {
+ // Same as C above.
+ id: "bookmarkGGGG",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "G",
+ bmkUri: "http://example.com/g",
+ dateAdded: now - 5000,
+ modified: now / 1000,
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ localTimeSeconds: now / 1000,
+ remoteTimeSeconds: now / 1000,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ ],
+ "Should leave roots with new remote structure unmerged"
+ );
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ ]);
+ deepEqual(
+ changesToUpload,
+ {
+ // We took the remote structure for the roots, but they're still flagged as
+ // changed locally. Since we always use the local state for roots
+ // (bug 1472241), and can't distinguish between value and structure changes
+ // in Places (see the comment for F below), we'll reupload them.
+ menu: {
+ tombstone: false,
+ counter: 2,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ children: ["folderDDDDDD"],
+ title: BookmarksMenuTitle,
+ },
+ },
+ mobile: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "mobile",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.mobileGuid),
+ children: ["bookmarkAAAA"],
+ title: MobileBookmarksTitle,
+ },
+ },
+ unfiled: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "unfiled",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
+ children: ["folderBBBBBB"],
+ title: UnfiledBookmarksTitle,
+ },
+ },
+ toolbar: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "toolbar",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.toolbarGuid),
+ children: ["folderFFFFFF", "bookmarkEEEE", "folderHHHHHH"],
+ title: BookmarksToolbarTitle,
+ },
+ },
+ },
+ "Should only reupload local roots"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "D",
+ children: [
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "G",
+ url: "http://example.com/g",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "folderFFFFFF",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "F",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ {
+ guid: "folderHHHHHH",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 2,
+ title: "H",
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ children: [
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "B",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ ],
+ },
+ "Should use newer remote parents and order"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_newer_local_moves() {
+ let now = Date.now();
+ let buf = await openMirror("newer_local_moves");
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ url: "http://example.com/a",
+ title: "A",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "B",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ url: "http://example.com/c",
+ title: "C",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ },
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "D",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ guid: "bookmarkEEEE",
+ url: "http://example.com/e",
+ title: "E",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ {
+ guid: "folderFFFFFF",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "F",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ children: [
+ {
+ guid: "bookmarkGGGG",
+ url: "http://example.com/g",
+ title: "G",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ },
+ {
+ guid: "folderHHHHHH",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "H",
+ dateAdded: new Date(now - 5000),
+ lastModified: new Date(now - 5000),
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA", "folderBBBBBB", "folderDDDDDD"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "menu",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "menu",
+ type: "folder",
+ title: "B",
+ children: ["bookmarkCCCC"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderBBBBBB",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkEEEE", "folderFFFFFF", "folderHHHHHH"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkEEEE",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "E",
+ bmkUri: "http://example.com/e",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderFFFFFF",
+ parentid: "toolbar",
+ type: "folder",
+ title: "F",
+ children: ["bookmarkGGGG"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "bookmarkGGGG",
+ parentid: "folderFFFFFF",
+ type: "bookmark",
+ title: "G",
+ bmkUri: "http://example.com/g",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ {
+ id: "folderHHHHHH",
+ parentid: "toolbar",
+ type: "folder",
+ title: "H",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 5,
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ info(
+ "Make local changes: Unfiled > A, Mobile > B; Toolbar > (H F E); D > C; H > G"
+ );
+ let localMoves = [
+ {
+ guid: "bookmarkAAAA",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ },
+ {
+ guid: "folderBBBBBB",
+ parentGuid: PlacesUtils.bookmarks.mobileGuid,
+ },
+ {
+ guid: "bookmarkCCCC",
+ parentGuid: "folderDDDDDD",
+ },
+ {
+ guid: "bookmarkGGGG",
+ parentGuid: "folderHHHHHH",
+ },
+ ];
+ for (let { guid, parentGuid } of localMoves) {
+ await PlacesUtils.bookmarks.update({
+ guid,
+ parentGuid,
+ index: 0,
+ lastModified: new Date(now),
+ });
+ }
+ await PlacesUtils.bookmarks.reorder(
+ PlacesUtils.bookmarks.toolbarGuid,
+ ["folderHHHHHH", "folderFFFFFF", "bookmarkEEEE"],
+ { lastModified: new Date(now) }
+ );
+
+ info(
+ "Make remote changes: Mobile > A, Unfiled > B; Toolbar > (F E H); D > G; H > C"
+ );
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderDDDDDD"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "mobile",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkAAAA"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "bookmarkAAAA",
+ parentid: "mobile",
+ type: "bookmark",
+ title: "A",
+ bmkUri: "http://example.com/a",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "unfiled",
+ parentid: "places",
+ type: "folder",
+ children: ["folderBBBBBB"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "folderBBBBBB",
+ parentid: "unfiled",
+ type: "folder",
+ title: "B",
+ children: [],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["folderFFFFFF", "bookmarkEEEE", "folderHHHHHH"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "folderHHHHHH",
+ parentid: "toolbar",
+ type: "folder",
+ title: "H",
+ children: ["bookmarkCCCC"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "bookmarkCCCC",
+ parentid: "folderHHHHHH",
+ type: "bookmark",
+ title: "C",
+ bmkUri: "http://example.com/c",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "folderDDDDDD",
+ parentid: "menu",
+ type: "folder",
+ title: "D",
+ children: ["bookmarkGGGG"],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "folderFFFFFF",
+ parentid: "toolbar",
+ type: "folder",
+ title: "F",
+ children: [],
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ {
+ id: "bookmarkGGGG",
+ parentid: "folderDDDDDD",
+ type: "bookmark",
+ title: "G",
+ bmkUri: "http://example.com/g",
+ dateAdded: now - 5000,
+ modified: now / 1000 - 2.5,
+ },
+ ])
+ );
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ localTimeSeconds: now / 1000,
+ remoteTimeSeconds: now / 1000,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ [
+ "bookmarkAAAA",
+ "bookmarkCCCC",
+ "bookmarkGGGG",
+ "folderBBBBBB",
+ "folderDDDDDD",
+ "folderFFFFFF",
+ "folderHHHHHH",
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ ],
+ "Should leave items with new remote structure unmerged"
+ );
+ let datesAdded = await promiseManyDatesAdded([
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ ]);
+ deepEqual(
+ changesToUpload,
+ {
+ // Reupload roots with new children.
+ menu: {
+ tombstone: false,
+ counter: 2,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.menuGuid),
+ children: ["folderDDDDDD"],
+ title: BookmarksMenuTitle,
+ },
+ },
+ mobile: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "mobile",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.mobileGuid),
+ children: ["folderBBBBBB"],
+ title: MobileBookmarksTitle,
+ },
+ },
+ unfiled: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "unfiled",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.unfiledGuid),
+ children: ["bookmarkAAAA"],
+ title: UnfiledBookmarksTitle,
+ },
+ },
+ toolbar: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "toolbar",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: datesAdded.get(PlacesUtils.bookmarks.toolbarGuid),
+ children: ["folderHHHHHH", "folderFFFFFF", "bookmarkEEEE"],
+ title: BookmarksToolbarTitle,
+ },
+ },
+ // G moved to H from F, so F and H have new children, and we need
+ // to upload G for the new `parentid`.
+ folderFFFFFF: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderFFFFFF",
+ type: "folder",
+ parentid: "toolbar",
+ hasDupe: true,
+ parentName: BookmarksToolbarTitle,
+ dateAdded: now - 5000,
+ children: [],
+ title: "F",
+ },
+ },
+ folderHHHHHH: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderHHHHHH",
+ type: "folder",
+ parentid: "toolbar",
+ hasDupe: true,
+ parentName: BookmarksToolbarTitle,
+ dateAdded: now - 5000,
+ children: ["bookmarkGGGG"],
+ title: "H",
+ },
+ },
+ bookmarkGGGG: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkGGGG",
+ type: "bookmark",
+ parentid: "folderHHHHHH",
+ hasDupe: true,
+ parentName: "H",
+ dateAdded: now - 5000,
+ bmkUri: "http://example.com/g",
+ title: "G",
+ },
+ },
+ // C moved to D, so we need to reupload D (for `children`) and C
+ // (for `parentid`).
+ folderDDDDDD: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderDDDDDD",
+ type: "folder",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: now - 5000,
+ children: ["bookmarkCCCC"],
+ title: "D",
+ },
+ },
+ bookmarkCCCC: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkCCCC",
+ type: "bookmark",
+ parentid: "folderDDDDDD",
+ hasDupe: true,
+ parentName: "D",
+ dateAdded: now - 5000,
+ bmkUri: "http://example.com/c",
+ title: "C",
+ },
+ },
+ // Reupload A with the new `parentid`. B moved to mobile *and* has
+ // new children` so we should upload it, anyway.
+ bookmarkAAAA: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkAAAA",
+ type: "bookmark",
+ parentid: "unfiled",
+ hasDupe: true,
+ parentName: UnfiledBookmarksTitle,
+ dateAdded: now - 5000,
+ bmkUri: "http://example.com/a",
+ title: "A",
+ },
+ },
+ folderBBBBBB: {
+ tombstone: false,
+ counter: 2,
+ synced: false,
+ cleartext: {
+ id: "folderBBBBBB",
+ type: "folder",
+ parentid: "mobile",
+ hasDupe: true,
+ parentName: MobileBookmarksTitle,
+ dateAdded: now - 5000,
+ children: [],
+ title: "B",
+ },
+ },
+ },
+ "Should reupload new local structure"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "folderDDDDDD",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "D",
+ children: [
+ {
+ guid: "bookmarkCCCC",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "C",
+ url: "http://example.com/c",
+ },
+ ],
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "folderHHHHHH",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "H",
+ children: [
+ {
+ guid: "bookmarkGGGG",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "G",
+ url: "http://example.com/g",
+ },
+ ],
+ },
+ {
+ guid: "folderFFFFFF",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: "F",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 2,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ children: [
+ {
+ guid: "bookmarkAAAA",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "A",
+ url: "http://example.com/a",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ children: [
+ {
+ guid: "folderBBBBBB",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "B",
+ },
+ ],
+ },
+ ],
+ },
+ "Should use newer local parents and order"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});
+
+add_task(async function test_unchanged_newer_changed_older() {
+ let buf = await openMirror("unchanged_newer_changed_older");
+ let modified = new Date(Date.now() - 5000);
+
+ info("Set up mirror");
+ await PlacesUtils.bookmarks.update({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ dateAdded: new Date(modified.getTime() - 5000),
+ });
+ await PlacesUtils.bookmarks.update({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ dateAdded: new Date(modified.getTime() - 5000),
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ guid: "folderAAAAAA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "A",
+ dateAdded: new Date(modified.getTime() - 5000),
+ lastModified: modified,
+ },
+ {
+ guid: "bookmarkBBBB",
+ url: "http://example.com/b",
+ title: "B",
+ dateAdded: new Date(modified.getTime() - 5000),
+ lastModified: modified,
+ },
+ ],
+ });
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ guid: "folderCCCCCC",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "C",
+ dateAdded: new Date(modified.getTime() - 5000),
+ lastModified: modified,
+ },
+ {
+ guid: "bookmarkDDDD",
+ url: "http://example.com/d",
+ title: "D",
+ dateAdded: new Date(modified.getTime() - 5000),
+ lastModified: modified,
+ },
+ ],
+ });
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["folderAAAAAA", "bookmarkBBBB"],
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000,
+ },
+ {
+ id: "folderAAAAAA",
+ parentid: "menu",
+ type: "folder",
+ title: "A",
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000,
+ },
+ {
+ id: "bookmarkBBBB",
+ parentid: "menu",
+ type: "bookmark",
+ title: "B",
+ bmkUri: "http://example.com/b",
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000,
+ },
+ {
+ id: "toolbar",
+ parentid: "places",
+ type: "folder",
+ children: ["folderCCCCCC", "bookmarkDDDD"],
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000,
+ },
+ {
+ id: "folderCCCCCC",
+ parentid: "toolbar",
+ type: "folder",
+ title: "C",
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000,
+ },
+ {
+ id: "bookmarkDDDD",
+ parentid: "toolbar",
+ type: "bookmark",
+ title: "D",
+ bmkUri: "http://example.com/d",
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000,
+ },
+ ]),
+ { needsMerge: false }
+ );
+ await PlacesTestUtils.markBookmarksAsSynced();
+
+ // Even though the local menu is newer (local = 5s, remote = 9s; adding E
+ // updated the modified times of A and the menu), it's not *changed* locally,
+ // so we should merge remote children first.
+ info("Add A > E locally with newer time; delete A remotely with older time");
+ await PlacesUtils.bookmarks.insert({
+ guid: "bookmarkEEEE",
+ parentGuid: "folderAAAAAA",
+ url: "http://example.com/e",
+ title: "E",
+ index: 0,
+ dateAdded: new Date(modified.getTime() + 5000),
+ lastModified: new Date(modified.getTime() + 5000),
+ });
+ await storeRecords(buf, [
+ {
+ id: "menu",
+ parentid: "places",
+ type: "folder",
+ children: ["bookmarkBBBB"],
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000 + 1,
+ },
+ {
+ id: "folderAAAAAA",
+ deleted: true,
+ },
+ ]);
+
+ // Even though the remote toolbar is newer (local = 15s, remote = 10s), it's
+ // not changed remotely, so we should merge local children first.
+ info("Add C > F remotely with newer time; delete C locally with older time");
+ await storeRecords(
+ buf,
+ shuffle([
+ {
+ id: "folderCCCCCC",
+ parentid: "toolbar",
+ type: "folder",
+ title: "C",
+ children: ["bookmarkFFFF"],
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000 + 5,
+ },
+ {
+ id: "bookmarkFFFF",
+ parentid: "folderCCCCCC",
+ type: "bookmark",
+ title: "F",
+ bmkUri: "http://example.com/f",
+ dateAdded: modified.getTime() - 5000,
+ modified: modified.getTime() / 1000 + 5,
+ },
+ ])
+ );
+ await PlacesUtils.bookmarks.remove("folderCCCCCC");
+ await PlacesUtils.bookmarks.update({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ lastModified: new Date(modified.getTime() - 5000),
+ // Use `SOURCES.SYNC` to avoid bumping the change counter and flagging the
+ // local toolbar as modified.
+ source: PlacesUtils.bookmarks.SOURCES.SYNC,
+ });
+
+ info("Apply remote");
+ let changesToUpload = await buf.apply({
+ localTimeSeconds: modified.getTime() / 1000 + 10,
+ remoteTimeSeconds: modified.getTime() / 1000 + 10,
+ });
+ deepEqual(
+ await buf.fetchUnmergedGuids(),
+ ["bookmarkFFFF", "folderCCCCCC", PlacesUtils.bookmarks.menuGuid],
+ "Should leave deleted C; F and menu with new remote structure unmerged"
+ );
+
+ deepEqual(
+ changesToUpload,
+ {
+ menu: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "menu",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: modified.getTime() - 5000,
+ children: ["bookmarkBBBB", "bookmarkEEEE"],
+ title: BookmarksMenuTitle,
+ },
+ },
+ toolbar: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "toolbar",
+ type: "folder",
+ parentid: "places",
+ hasDupe: true,
+ parentName: "",
+ dateAdded: modified.getTime() - 5000,
+ children: ["bookmarkDDDD", "bookmarkFFFF"],
+ title: BookmarksToolbarTitle,
+ },
+ },
+ // Upload E and F with new `parentid`.
+ bookmarkEEEE: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkEEEE",
+ type: "bookmark",
+ parentid: "menu",
+ hasDupe: true,
+ parentName: BookmarksMenuTitle,
+ dateAdded: modified.getTime() + 5000,
+ bmkUri: "http://example.com/e",
+ title: "E",
+ },
+ },
+ bookmarkFFFF: {
+ tombstone: false,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "bookmarkFFFF",
+ type: "bookmark",
+ parentid: "toolbar",
+ hasDupe: true,
+ parentName: BookmarksToolbarTitle,
+ dateAdded: modified.getTime() - 5000,
+ bmkUri: "http://example.com/f",
+ title: "F",
+ },
+ },
+ folderCCCCCC: {
+ tombstone: true,
+ counter: 1,
+ synced: false,
+ cleartext: {
+ id: "folderCCCCCC",
+ deleted: true,
+ },
+ },
+ },
+ "Should reupload menu, toolbar, E, F with new structure; tombstone for C"
+ );
+
+ await assertLocalTree(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: "",
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 0,
+ title: BookmarksMenuTitle,
+ children: [
+ {
+ guid: "bookmarkBBBB",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "B",
+ url: "http://example.com/b",
+ },
+ {
+ guid: "bookmarkEEEE",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "E",
+ url: "http://example.com/e",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 1,
+ title: BookmarksToolbarTitle,
+ children: [
+ {
+ guid: "bookmarkDDDD",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 0,
+ title: "D",
+ url: "http://example.com/d",
+ },
+ {
+ guid: "bookmarkFFFF",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ index: 1,
+ title: "F",
+ url: "http://example.com/f",
+ },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 3,
+ title: UnfiledBookmarksTitle,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ index: 4,
+ title: MobileBookmarksTitle,
+ },
+ ],
+ },
+ "Should merge children of changed side first, even if they're older"
+ );
+
+ let tombstones = await PlacesTestUtils.fetchSyncTombstones();
+ deepEqual(
+ tombstones.map(({ guid }) => guid),
+ ["folderCCCCCC"],
+ "Should store local tombstone for C"
+ );
+
+ await storeChangesInMirror(buf, changesToUpload);
+ deepEqual(await buf.fetchUnmergedGuids(), [], "Should merge all items");
+
+ await buf.finalize();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesSyncUtils.bookmarks.reset();
+});