summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/unit/test_async_transactions.js
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/places/tests/unit/test_async_transactions.js
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/places/tests/unit/test_async_transactions.js')
-rw-r--r--toolkit/components/places/tests/unit/test_async_transactions.js2237
1 files changed, 2237 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/unit/test_async_transactions.js b/toolkit/components/places/tests/unit/test_async_transactions.js
new file mode 100644
index 0000000000..f016b5b808
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -0,0 +1,2237 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const bmsvc = PlacesUtils.bookmarks;
+const obsvc = PlacesUtils.observers;
+const tagssvc = PlacesUtils.tagging;
+const PT = PlacesTransactions;
+const menuGuid = PlacesUtils.bookmarks.menuGuid;
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+// Create and add bookmarks observer.
+var observer = {
+ tagRelatedGuids: new Set(),
+
+ reset() {
+ this.itemsAdded = new Map();
+ this.itemsRemoved = new Map();
+ this.itemsChanged = new Map();
+ this.itemsMoved = new Map();
+ this.itemsTitleChanged = new Map();
+ this.itemsUrlChanged = new Map();
+ },
+
+ handlePlacesEvents(events) {
+ for (let event of events) {
+ switch (event.type) {
+ case "bookmark-added":
+ // Ignore tag items.
+ if (event.isTagging) {
+ this.tagRelatedGuids.add(event.guid);
+ return;
+ }
+
+ this.itemsAdded.set(event.guid, {
+ itemId: event.id,
+ parentGuid: event.parentGuid,
+ index: event.index,
+ itemType: event.itemType,
+ title: event.title,
+ url: event.url,
+ });
+ break;
+ case "bookmark-removed":
+ if (this.tagRelatedGuids.has(event.guid)) {
+ return;
+ }
+
+ this.itemsRemoved.set(event.guid, {
+ parentGuid: event.parentGuid,
+ index: event.index,
+ itemType: event.itemType,
+ });
+ break;
+ case "bookmark-moved":
+ this.itemsMoved.set(event.guid, {
+ oldParentGuid: event.oldParentGuid,
+ oldIndex: event.oldIndex,
+ newParentGuid: event.parentGuid,
+ newIndex: event.index,
+ itemType: event.itemType,
+ });
+ break;
+ case "bookmark-title-changed":
+ if (this.tagRelatedGuids.has(event.guid)) {
+ return;
+ }
+
+ this.itemsTitleChanged.set(event.guid, {
+ title: event.title,
+ parentGuid: event.parentGuid,
+ });
+ break;
+ case "bookmark-url-changed":
+ if (this.tagRelatedGuids.has(event.guid)) {
+ return;
+ }
+
+ this.itemsUrlChanged.set(event.guid, {
+ url: event.url,
+ });
+ break;
+ }
+ }
+ },
+
+ onItemChanged(
+ aItemId,
+ aProperty,
+ aIsAnnoProperty,
+ aNewValue,
+ aLastModified,
+ aItemType,
+ aParentId,
+ aGuid,
+ aParentGuid
+ ) {
+ if (this.tagRelatedGuids.has(aGuid)) {
+ return;
+ }
+
+ let changesForGuid = this.itemsChanged.get(aGuid);
+ if (changesForGuid === undefined) {
+ changesForGuid = new Map();
+ this.itemsChanged.set(aGuid, changesForGuid);
+ }
+ let change = {
+ isAnnoProperty: aIsAnnoProperty,
+ newValue: aNewValue,
+ lastModified: aLastModified,
+ itemType: aItemType,
+ };
+ changesForGuid.set(aProperty, change);
+ },
+};
+Object.setPrototypeOf(observer, NavBookmarkObserver.prototype);
+observer.reset();
+
+// index at which items should begin
+var bmStartIndex = 0;
+
+function run_test() {
+ bmsvc.addObserver(observer);
+ observer.handlePlacesEvents = observer.handlePlacesEvents.bind(observer);
+ obsvc.addListener(
+ [
+ "bookmark-added",
+ "bookmark-removed",
+ "bookmark-moved",
+ "bookmark-title-changed",
+ "bookmark-url-changed",
+ ],
+ observer.handlePlacesEvents
+ );
+ registerCleanupFunction(function() {
+ bmsvc.removeObserver(observer);
+ obsvc.removeListener(
+ [
+ "bookmark-added",
+ "bookmark-removed",
+ "bookmark-moved",
+ "bookmark-title-changed",
+ "bookmark-url-changed",
+ ],
+ observer.handlePlacesEvents
+ );
+ });
+
+ run_next_test();
+}
+
+function sanityCheckTransactionHistory() {
+ Assert.ok(PT.undoPosition <= PT.length);
+
+ let check_entry_throws = f => {
+ try {
+ f();
+ do_throw("PT.entry should throw for invalid input");
+ } catch (ex) {}
+ };
+ check_entry_throws(() => PT.entry(-1));
+ check_entry_throws(() => PT.entry({}));
+ check_entry_throws(() => PT.entry(PT.length));
+
+ if (PT.undoPosition < PT.length) {
+ Assert.equal(PT.topUndoEntry, PT.entry(PT.undoPosition));
+ } else {
+ Assert.equal(null, PT.topUndoEntry);
+ }
+ if (PT.undoPosition > 0) {
+ Assert.equal(PT.topRedoEntry, PT.entry(PT.undoPosition - 1));
+ } else {
+ Assert.equal(null, PT.topRedoEntry);
+ }
+}
+
+function getTransactionsHistoryState() {
+ let history = [];
+ for (let i = 0; i < PT.length; i++) {
+ history.push(PT.entry(i));
+ }
+ return [history, PT.undoPosition];
+}
+
+function ensureUndoState(aExpectedEntries = [], aExpectedUndoPosition = 0) {
+ // ensureUndoState is called in various places during this test, so it's
+ // a good places to sanity-check the transaction-history APIs in all
+ // cases.
+ sanityCheckTransactionHistory();
+
+ let [actualEntries, actualUndoPosition] = getTransactionsHistoryState();
+ Assert.equal(actualEntries.length, aExpectedEntries.length);
+ Assert.equal(actualUndoPosition, aExpectedUndoPosition);
+
+ function checkEqualEntries(aExpectedEntry, aActualEntry) {
+ Assert.equal(aExpectedEntry.length, aActualEntry.length);
+ aExpectedEntry.forEach((t, i) => Assert.equal(t, aActualEntry[i]));
+ }
+ aExpectedEntries.forEach((e, i) => checkEqualEntries(e, actualEntries[i]));
+}
+
+function ensureItemsAdded(...items) {
+ let expectedResultsCount = items.length;
+
+ for (let item of items) {
+ if ("children" in item) {
+ expectedResultsCount += item.children.length;
+ }
+ Assert.ok(
+ observer.itemsAdded.has(item.guid),
+ `Should have the expected guid ${item.guid}`
+ );
+ let info = observer.itemsAdded.get(item.guid);
+ Assert.equal(
+ info.parentGuid,
+ item.parentGuid,
+ "Should have notified the correct parentGuid"
+ );
+ for (let propName of ["title", "index", "itemType"]) {
+ if (propName in item) {
+ Assert.equal(info[propName], item[propName]);
+ }
+ }
+ if ("url" in item) {
+ Assert.ok(
+ Services.io.newURI(info.url).equals(Services.io.newURI(item.url)),
+ "Should have the correct url"
+ );
+ }
+ }
+
+ Assert.equal(
+ observer.itemsAdded.size,
+ expectedResultsCount,
+ "Should have added the correct number of items"
+ );
+}
+
+function ensureItemsRemoved(...items) {
+ let expectedResultsCount = items.length;
+
+ for (let item of items) {
+ // We accept both guids and full info object here.
+ if (typeof item == "string") {
+ Assert.ok(
+ observer.itemsRemoved.has(item),
+ `Should have removed the expected guid ${item}`
+ );
+ } else {
+ if ("children" in item) {
+ expectedResultsCount += item.children.length;
+ }
+
+ Assert.ok(
+ observer.itemsRemoved.has(item.guid),
+ `Should have removed expected guid ${item.guid}`
+ );
+ let info = observer.itemsRemoved.get(item.guid);
+ Assert.equal(
+ info.parentGuid,
+ item.parentGuid,
+ "Should have notified the correct parentGuid"
+ );
+ if ("index" in item) {
+ Assert.equal(info.index, item.index);
+ }
+ }
+ }
+
+ Assert.equal(
+ observer.itemsRemoved.size,
+ expectedResultsCount,
+ "Should have removed the correct number of items"
+ );
+}
+
+function ensureItemsChanged(...items) {
+ for (let item of items) {
+ Assert.ok(observer.itemsChanged.has(item.guid));
+ let changes = observer.itemsChanged.get(item.guid);
+ Assert.ok(changes.has(item.property));
+ let info = changes.get(item.property);
+ Assert.ok(!info.isAnnoProperty);
+ Assert.equal(info.newValue, item.newValue);
+ if ("url" in item) {
+ Assert.ok(item.url.equals(info.url));
+ }
+ }
+}
+
+function ensureItemsMoved(...items) {
+ Assert.equal(
+ observer.itemsMoved.size,
+ items.length,
+ "Should have received the correct number of moved notifications"
+ );
+ for (let item of items) {
+ Assert.ok(
+ observer.itemsMoved.has(item.guid),
+ `Observer should have a move for ${item.guid}`
+ );
+ let info = observer.itemsMoved.get(item.guid);
+ Assert.equal(
+ info.oldParentGuid,
+ item.oldParentGuid,
+ "Should have the correct old parent guid"
+ );
+ Assert.equal(
+ info.oldIndex,
+ item.oldIndex,
+ "Should have the correct old index"
+ );
+ Assert.equal(
+ info.newParentGuid,
+ item.newParentGuid,
+ "Should have the correct new parent guid"
+ );
+ Assert.equal(
+ info.newIndex,
+ item.newIndex,
+ "Should have the correct new index"
+ );
+ }
+}
+
+function ensureItemsTitleChanged(...items) {
+ Assert.equal(
+ observer.itemsTitleChanged.size,
+ items.length,
+ "Should have received the correct number of bookmark-title-changed notifications"
+ );
+ for (const item of items) {
+ Assert.ok(
+ observer.itemsTitleChanged.has(item.guid),
+ `Observer should have a title changed for ${item.guid}`
+ );
+ const info = observer.itemsTitleChanged.get(item.guid);
+ Assert.equal(info.title, item.title, "Should have the correct title");
+ Assert.equal(
+ info.parentGuid,
+ item.parentGuid,
+ "Should have the correct parent guid"
+ );
+ }
+}
+
+function ensureItemsUrlChanged(...items) {
+ Assert.equal(
+ observer.itemsUrlChanged.size,
+ items.length,
+ "Should have received the correct number of bookmark-url-changed notifications"
+ );
+ for (const item of items) {
+ Assert.ok(
+ observer.itemsUrlChanged.has(item.guid),
+ `Observer should have a title changed for ${item.guid}`
+ );
+ const info = observer.itemsUrlChanged.get(item.guid);
+ Assert.equal(info.url, item.url, "Should have the correct url");
+ }
+}
+
+function ensureTagsForURI(aURI, aTags) {
+ let tagsSet = tagssvc.getTagsForURI(Services.io.newURI(aURI));
+ Assert.equal(tagsSet.length, aTags.length);
+ Assert.ok(aTags.every(t => tagsSet.includes(t)));
+}
+
+function createTestFolderInfo(
+ title = "Test Folder",
+ parentGuid = menuGuid,
+ children = undefined
+) {
+ let info = { parentGuid, title };
+ if (children) {
+ info.children = children;
+ }
+ return info;
+}
+
+function removeAllDatesInTree(tree) {
+ if ("lastModified" in tree) {
+ delete tree.lastModified;
+ }
+ if ("dateAdded" in tree) {
+ delete tree.dateAdded;
+ }
+
+ if (!tree.children) {
+ return;
+ }
+
+ for (let child of tree.children) {
+ removeAllDatesInTree(child);
+ }
+}
+
+// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the
+// same.
+// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set.
+async function ensureEqualBookmarksTrees(
+ aOriginal,
+ aNew,
+ aIsRestoredItem = true,
+ aCheckParentAndPosition = false,
+ aIgnoreAllDates = false
+) {
+ // Note "id" is not-enumerable, and is therefore skipped by Object.keys (both
+ // ours and the one at deepEqual). This is fine for us because ids are not
+ // restored by Redo.
+ if (aIsRestoredItem) {
+ if (aIgnoreAllDates) {
+ removeAllDatesInTree(aOriginal);
+ removeAllDatesInTree(aNew);
+ } else if (!aOriginal.lastModified) {
+ // Ignore lastModified for newly created items, for performance reasons.
+ aNew.lastModified = aOriginal.lastModified;
+ }
+ Assert.deepEqual(aOriginal, aNew);
+ return;
+ }
+
+ for (let property of Object.keys(aOriginal)) {
+ if (property == "children") {
+ Assert.equal(aOriginal.children.length, aNew.children.length);
+ for (let i = 0; i < aOriginal.children.length; i++) {
+ await ensureEqualBookmarksTrees(
+ aOriginal.children[i],
+ aNew.children[i],
+ false,
+ true,
+ aIgnoreAllDates
+ );
+ }
+ } else if (property == "guid") {
+ // guid shouldn't be copied if the item was not restored.
+ Assert.notEqual(aOriginal.guid, aNew.guid);
+ } else if (property == "dateAdded") {
+ // dateAdded shouldn't be copied if the item was not restored.
+ Assert.ok(is_time_ordered(aOriginal.dateAdded, aNew.dateAdded));
+ } else if (property == "lastModified") {
+ // same same, except for the never-changed case
+ if (!aOriginal.lastModified) {
+ Assert.ok(!aNew.lastModified);
+ } else {
+ Assert.ok(is_time_ordered(aOriginal.lastModified, aNew.lastModified));
+ }
+ } else if (
+ aCheckParentAndPosition ||
+ (property != "parentGuid" && property != "index")
+ ) {
+ Assert.deepEqual(aOriginal[property], aNew[property]);
+ }
+ }
+}
+
+async function ensureBookmarksTreeRestoredCorrectly(
+ ...aOriginalBookmarksTrees
+) {
+ for (let originalTree of aOriginalBookmarksTrees) {
+ let restoredTree = await PlacesUtils.promiseBookmarksTree(
+ originalTree.guid
+ );
+ await ensureEqualBookmarksTrees(originalTree, restoredTree);
+ }
+}
+
+async function ensureBookmarksTreeRestoredCorrectlyExceptDates(
+ ...aOriginalBookmarksTrees
+) {
+ for (let originalTree of aOriginalBookmarksTrees) {
+ let restoredTree = await PlacesUtils.promiseBookmarksTree(
+ originalTree.guid
+ );
+ await ensureEqualBookmarksTrees(
+ originalTree,
+ restoredTree,
+ true,
+ false,
+ true
+ );
+ }
+}
+
+async function ensureNonExistent(...aGuids) {
+ for (let guid of aGuids) {
+ Assert.strictEqual(await PlacesUtils.promiseBookmarksTree(guid), null);
+ }
+}
+
+add_task(async function test_recycled_transactions() {
+ async function ensureTransactThrowsFor(aTransaction) {
+ let [txns, undoPosition] = getTransactionsHistoryState();
+ try {
+ await aTransaction.transact();
+ do_throw("Shouldn't be able to use the same transaction twice");
+ } catch (ex) {}
+ ensureUndoState(txns, undoPosition);
+ }
+
+ let txn_a = PT.NewFolder(createTestFolderInfo());
+ await txn_a.transact();
+ ensureUndoState([[txn_a]], 0);
+ await ensureTransactThrowsFor(txn_a);
+
+ await PT.undo();
+ ensureUndoState([[txn_a]], 1);
+ ensureTransactThrowsFor(txn_a);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+ ensureTransactThrowsFor(txn_a);
+
+ let txn_b = PT.NewFolder(createTestFolderInfo());
+ await PT.batch(async function() {
+ try {
+ await txn_a.transact();
+ do_throw("Shouldn't be able to use the same transaction twice");
+ } catch (ex) {}
+ ensureUndoState();
+ await txn_b.transact();
+ });
+ ensureUndoState([[txn_b]], 0);
+
+ await PT.undo();
+ ensureUndoState([[txn_b]], 1);
+ ensureTransactThrowsFor(txn_a);
+ ensureTransactThrowsFor(txn_b);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+ observer.reset();
+});
+
+add_task(async function test_new_folder_with_children() {
+ let folder_info = createTestFolderInfo(
+ "Test folder",
+ PlacesUtils.bookmarks.menuGuid,
+ [
+ {
+ url: "http://test_create_item.com",
+ title: "Test creating an item",
+ },
+ ]
+ );
+ ensureUndoState();
+ let txn = PT.NewFolder(folder_info);
+ folder_info.guid = await txn.transact();
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(folder_info.guid);
+ let ensureDo = async function(aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ ensureItemsAdded(folder_info);
+ if (aRedo) {
+ // Ignore lastModified in the comparison, for performance reasons.
+ originalInfo.lastModified = null;
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo);
+ }
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({
+ guid: folder_info.guid,
+ parentGuid: folder_info.parentGuid,
+ index: bmStartIndex,
+ children: [
+ {
+ title: "Test creating an item",
+ url: "http://test_create_item.com",
+ },
+ ],
+ });
+ observer.reset();
+ };
+
+ await ensureDo();
+ await PT.undo();
+ await ensureUndo();
+ await PT.redo();
+ await ensureDo(true);
+ await PT.undo();
+ ensureUndo();
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_new_bookmark() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test_create_item.com",
+ index: bmStartIndex,
+ title: "Test creating an item",
+ };
+
+ ensureUndoState();
+ let txn = PT.NewBookmark(bm_info);
+ bm_info.guid = await txn.transact();
+
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(bm_info.guid);
+ let ensureDo = async function(aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ await ensureItemsAdded(bm_info);
+ if (aRedo) {
+ await ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ }
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({
+ guid: bm_info.guid,
+ parentGuid: bm_info.parentGuid,
+ index: bmStartIndex,
+ });
+ observer.reset();
+ };
+
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo(true);
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_merge_create_folder_and_item() {
+ let folder_info = createTestFolderInfo();
+ let bm_info = {
+ url: "http://test_create_item_to_folder.com",
+ title: "Test Bookmark",
+ index: bmStartIndex,
+ };
+
+ let [folderTxnResult, bkmTxnResult] = await PT.batch(async function() {
+ let folderTxn = PT.NewFolder(folder_info);
+ folder_info.guid = bm_info.parentGuid = await folderTxn.transact();
+ let bkmTxn = PT.NewBookmark(bm_info);
+ bm_info.guid = await bkmTxn.transact();
+ return [folderTxn, bkmTxn];
+ });
+
+ let ensureDo = async function() {
+ ensureUndoState([[bkmTxnResult, folderTxnResult]], 0);
+ await ensureItemsAdded(folder_info, bm_info);
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[bkmTxnResult, folderTxnResult]], 1);
+ ensureItemsRemoved(folder_info, bm_info);
+ observer.reset();
+ };
+
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_move_items_to_folder() {
+ let folder_a_info = createTestFolderInfo("Folder A");
+ let bkm_a_info = { url: "http://test_move_items.com", title: "Bookmark A" };
+ let bkm_b_info = { url: "http://test_move_items.com", title: "Bookmark B" };
+
+ // Test moving items within the same folder.
+ let [
+ folder_a_txn_result,
+ bkm_a_txn_result,
+ bkm_b_txn_result,
+ ] = await PT.batch(async function() {
+ let folder_a_txn = PT.NewFolder(folder_a_info);
+
+ folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid = await folder_a_txn.transact();
+ let bkm_a_txn = PT.NewBookmark(bkm_a_info);
+ bkm_a_info.guid = await bkm_a_txn.transact();
+ let bkm_b_txn = PT.NewBookmark(bkm_b_info);
+ bkm_b_info.guid = await bkm_b_txn.transact();
+ return [folder_a_txn, bkm_a_txn, bkm_b_txn];
+ });
+
+ ensureUndoState(
+ [[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]],
+ 0
+ );
+
+ let moveTxn = PT.Move({
+ guid: bkm_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ });
+ await moveTxn.transact();
+
+ let ensureDo = () => {
+ ensureUndoState(
+ [[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]],
+ 0
+ );
+ ensureItemsMoved({
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 1,
+ });
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState(
+ [[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]],
+ 1
+ );
+ ensureItemsMoved({
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 1,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory(false, true);
+ ensureUndoState(
+ [[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]],
+ 0
+ );
+
+ // Test moving items between folders.
+ let folder_b_info = createTestFolderInfo("Folder B");
+ let folder_b_txn = PT.NewFolder(folder_b_info);
+ folder_b_info.guid = await folder_b_txn.transact();
+ ensureUndoState(
+ [[folder_b_txn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]],
+ 0
+ );
+
+ moveTxn = PT.Move({
+ guid: bkm_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ newIndex: bmsvc.DEFAULT_INDEX,
+ });
+ await moveTxn.transact();
+
+ ensureDo = () => {
+ ensureUndoState(
+ [
+ [moveTxn],
+ [folder_b_txn],
+ [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result],
+ ],
+ 0
+ );
+ ensureItemsMoved({
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+ ensureUndo = () => {
+ ensureUndoState(
+ [
+ [moveTxn],
+ [folder_b_txn],
+ [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result],
+ ],
+ 1
+ );
+ ensureItemsMoved({
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_b_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ // Clean up
+ await PT.undo(); // folder_b_txn
+ await PT.undo(); // folder_a_txn + the bookmarks;
+ Assert.equal(observer.itemsRemoved.size, 4);
+ ensureUndoState(
+ [
+ [moveTxn],
+ [folder_b_txn],
+ [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result],
+ ],
+ 3
+ );
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_move_multiple_items_to_folder() {
+ let folder_a_info = createTestFolderInfo("Folder A");
+ let bkm_a_info = { url: "http://test_move_items.com", title: "Bookmark A" };
+ let bkm_b_info = { url: "http://test_move_items.com", title: "Bookmark B" };
+ let bkm_c_info = { url: "http://test_move_items.com", title: "Bookmark C" };
+
+ // Test moving items within the same folder.
+ let [
+ folder_a_txn_result,
+ bkm_a_txn_result,
+ bkm_b_txn_result,
+ bkm_c_txn_result,
+ ] = await PT.batch(async function() {
+ let folder_a_txn = PT.NewFolder(folder_a_info);
+
+ folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid = bkm_c_info.parentGuid = await folder_a_txn.transact();
+ let bkm_a_txn = PT.NewBookmark(bkm_a_info);
+ bkm_a_info.guid = await bkm_a_txn.transact();
+ let bkm_b_txn = PT.NewBookmark(bkm_b_info);
+ bkm_b_info.guid = await bkm_b_txn.transact();
+ let bkm_c_txn = PT.NewBookmark(bkm_c_info);
+ bkm_c_info.guid = await bkm_c_txn.transact();
+ return [folder_a_txn, bkm_a_txn, bkm_b_txn, bkm_c_txn];
+ });
+
+ ensureUndoState(
+ [
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 0
+ );
+
+ let moveTxn = PT.Move({
+ guids: [bkm_a_info.guid, bkm_b_info.guid],
+ newParentGuid: folder_a_info.guid,
+ });
+ await moveTxn.transact();
+
+ let ensureDo = () => {
+ ensureUndoState(
+ [
+ [moveTxn],
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 0
+ );
+ ensureItemsMoved(
+ {
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 2,
+ },
+ {
+ guid: bkm_b_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 1,
+ newIndex: 2,
+ }
+ );
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState(
+ [
+ [moveTxn],
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 1
+ );
+ ensureItemsMoved(
+ {
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 1,
+ newIndex: 0,
+ },
+ {
+ guid: bkm_b_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 2,
+ newIndex: 1,
+ }
+ );
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory(false, true);
+ ensureUndoState(
+ [
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 0
+ );
+
+ // Test moving items between folders.
+ let folder_b_info = createTestFolderInfo("Folder B");
+ let folder_b_txn = PT.NewFolder(folder_b_info);
+ folder_b_info.guid = await folder_b_txn.transact();
+ ensureUndoState(
+ [
+ [folder_b_txn],
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 0
+ );
+
+ moveTxn = PT.Move({
+ guid: bkm_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ newIndex: bmsvc.DEFAULT_INDEX,
+ });
+ await moveTxn.transact();
+
+ ensureDo = () => {
+ ensureUndoState(
+ [
+ [moveTxn],
+ [folder_b_txn],
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 0
+ );
+ ensureItemsMoved({
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+ ensureUndo = () => {
+ ensureUndoState(
+ [
+ [moveTxn],
+ [folder_b_txn],
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 1
+ );
+ ensureItemsMoved({
+ guid: bkm_a_info.guid,
+ oldParentGuid: folder_b_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ // Clean up
+ await PT.undo(); // folder_b_txn
+ await PT.undo(); // folder_a_txn + the bookmarks;
+ Assert.equal(observer.itemsRemoved.size, 5);
+ ensureUndoState(
+ [
+ [moveTxn],
+ [folder_b_txn],
+ [
+ bkm_c_txn_result,
+ bkm_b_txn_result,
+ bkm_a_txn_result,
+ folder_a_txn_result,
+ ],
+ ],
+ 3
+ );
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_remove_folder() {
+ let folder_level_1_info = createTestFolderInfo("Folder Level 1");
+ let folder_level_2_info = { title: "Folder Level 2" };
+ let [folder_level_1_txn_result, folder_level_2_txn_result] = await PT.batch(
+ async function() {
+ let folder_level_1_txn = PT.NewFolder(folder_level_1_info);
+ folder_level_1_info.guid = await folder_level_1_txn.transact();
+ folder_level_2_info.parentGuid = folder_level_1_info.guid;
+ let folder_level_2_txn = PT.NewFolder(folder_level_2_info);
+ folder_level_2_info.guid = await folder_level_2_txn.transact();
+ return [folder_level_1_txn, folder_level_2_txn];
+ }
+ );
+
+ let original_folder_level_1_tree = await PlacesUtils.promiseBookmarksTree(
+ folder_level_1_info.guid
+ );
+ let original_folder_level_2_tree = Object.assign(
+ { parentGuid: original_folder_level_1_tree.guid },
+ original_folder_level_1_tree.children[0]
+ );
+
+ ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]);
+ await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ observer.reset();
+
+ let remove_folder_2_txn = PT.Remove(folder_level_2_info);
+ await remove_folder_2_txn.transact();
+
+ ensureUndoState([
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ]);
+ await ensureItemsRemoved(folder_level_2_info);
+
+ // Undo Remove "Folder Level 2"
+ await PT.undo();
+ ensureUndoState(
+ [
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ],
+ 1
+ );
+ await ensureItemsAdded(folder_level_2_info);
+ await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ await PT.redo();
+ ensureUndoState([
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ]);
+ await ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo it again
+ await PT.undo();
+ ensureUndoState(
+ [
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ],
+ 1
+ );
+ await ensureItemsAdded(folder_level_2_info);
+ await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
+ observer.reset();
+
+ // Undo the creation of both folders
+ await PT.undo();
+ ensureUndoState(
+ [
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ],
+ 2
+ );
+ await ensureItemsRemoved(folder_level_2_info, folder_level_1_info);
+ observer.reset();
+
+ // Redo the creation of both folders
+ await PT.redo();
+ ensureUndoState(
+ [
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ],
+ 1
+ );
+ await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(
+ original_folder_level_1_tree
+ );
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ await PT.redo();
+ ensureUndoState([
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ]);
+ await ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo everything one last time
+ await PT.undo();
+ ensureUndoState(
+ [
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ],
+ 1
+ );
+ await ensureItemsAdded(folder_level_2_info);
+ observer.reset();
+
+ await PT.undo();
+ ensureUndoState(
+ [
+ [remove_folder_2_txn],
+ [folder_level_2_txn_result, folder_level_1_txn_result],
+ ],
+ 2
+ );
+ await ensureItemsRemoved(folder_level_2_info, folder_level_2_info);
+ observer.reset();
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_add_and_remove_bookmarks_with_additional_info() {
+ const testURI = "http://add.remove.tag";
+ const TAG_1 = "TestTag1";
+ const TAG_2 = "TestTag2";
+
+ let folder_info = createTestFolderInfo();
+ folder_info.guid = await PT.NewFolder(folder_info).transact();
+ let ensureTags = ensureTagsForURI.bind(null, testURI);
+
+ // Check that the NewBookmark transaction preserves tags.
+ observer.reset();
+ let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
+ b1_info.guid = await PT.NewBookmark(b1_info).transact();
+ let b1_originalInfo = await PlacesUtils.promiseBookmarksTree(b1_info.guid);
+ ensureTags([TAG_1]);
+ await PT.undo();
+ ensureTags([]);
+
+ observer.reset();
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
+ ensureTags([TAG_1]);
+
+ // Check if the Remove transaction removes and restores tags of children
+ // correctly.
+ await PT.Remove(folder_info.guid).transact();
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
+ ensureTags([TAG_1]);
+
+ await PT.redo();
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
+ ensureTags([TAG_1]);
+
+ // * Check that no-op tagging (the uri is already tagged with TAG_1) is
+ // also a no-op on undo.
+ observer.reset();
+ let b2_info = {
+ parentGuid: folder_info.guid,
+ url: testURI,
+ tags: [TAG_1, TAG_2],
+ };
+ b2_info.guid = await PT.NewBookmark(b2_info).transact();
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureItemsRemoved(b2_info);
+ ensureTags([TAG_1]);
+
+ // Check if Remove correctly restores tags.
+ observer.reset();
+ await PT.redo();
+ ensureTags([TAG_1, TAG_2]);
+
+ // Test Remove for multiple items.
+ observer.reset();
+ await PT.Remove(b1_info.guid).transact();
+ await PT.Remove(b2_info.guid).transact();
+ await PT.Remove(folder_info.guid).transact();
+ await ensureItemsRemoved(b1_info, b2_info, folder_info);
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureItemsAdded(folder_info);
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureItemsAdded(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ // The redo calls below cleanup everything we did.
+ observer.reset();
+ await PT.redo();
+ await ensureItemsRemoved(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ await PT.redo();
+ // The tag containers are removed in async and take some time
+ let oldCountTag1 = 0;
+ let oldCountTag2 = 0;
+ let allTags = await bmsvc.fetchTags();
+ for (let i of allTags) {
+ if (i.name == TAG_1) {
+ oldCountTag1 = i.count;
+ }
+ if (i.name == TAG_2) {
+ oldCountTag2 = i.count;
+ }
+ }
+ await TestUtils.waitForCondition(async () => {
+ allTags = await bmsvc.fetchTags();
+ let newCountTag1 = 0;
+ let newCountTag2 = 0;
+ for (let i of allTags) {
+ if (i.name == TAG_1) {
+ newCountTag1 = i.count;
+ }
+ if (i.name == TAG_2) {
+ newCountTag2 = i.count;
+ }
+ }
+ return newCountTag1 == oldCountTag1 - 1 && newCountTag2 == oldCountTag2 - 1;
+ });
+ await ensureItemsRemoved(b2_info);
+
+ ensureTags([]);
+
+ observer.reset();
+ await PT.redo();
+ await ensureItemsRemoved(folder_info);
+ ensureTags([]);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_creating_and_removing_a_separator() {
+ let folder_info = createTestFolderInfo();
+ let separator_info = {};
+ let undoEntries = [];
+
+ observer.reset();
+ let create_txns = await PT.batch(async function() {
+ let folder_txn = PT.NewFolder(folder_info);
+ folder_info.guid = separator_info.parentGuid = await folder_txn.transact();
+ let separator_txn = PT.NewSeparator(separator_info);
+ separator_info.guid = await separator_txn.transact();
+ return [separator_txn, folder_txn];
+ });
+ undoEntries.unshift(create_txns);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ await PT.redo();
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ let remove_sep_txn = PT.Remove(separator_info);
+ await remove_sep_txn.transact();
+ undoEntries.unshift([remove_sep_txn]);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsRemoved(separator_info);
+
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(separator_info);
+
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 2);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ await PT.redo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(folder_info, separator_info);
+
+ // Clear redo entries and check that |redo| does nothing
+ observer.reset();
+ await PT.clearTransactionsHistory(false, true);
+ undoEntries.shift();
+ ensureUndoState(undoEntries, 0);
+ await PT.redo();
+ ensureItemsAdded();
+ ensureItemsRemoved();
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_title() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test_create_item.com",
+ title: "Original Title",
+ };
+
+ function ensureTitleChange(aCurrentTitle) {
+ ensureItemsTitleChanged({
+ guid: bm_info.guid,
+ title: aCurrentTitle,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact();
+ ensureTitleChange("New Title");
+
+ observer.reset();
+ await PT.undo();
+ ensureTitleChange("Original Title");
+
+ observer.reset();
+ await PT.redo();
+ ensureTitleChange("New Title");
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureTitleChange("Original Title");
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_url() {
+ let oldURI = "http://old.test_editing_item_uri.com/";
+ let newURI = "http://new.test_editing_item_uri.com/";
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: oldURI,
+ tags: ["TestTag"],
+ };
+ function ensureURIAndTags(
+ aPreChangeURI,
+ aPostChangeURI,
+ aOLdURITagsPreserved
+ ) {
+ ensureItemsUrlChanged({
+ guid: bm_info.guid,
+ url: aPostChangeURI,
+ });
+ ensureTagsForURI(aPostChangeURI, bm_info.tags);
+ ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []);
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ ensureTagsForURI(oldURI, bm_info.tags);
+
+ // When there's a single bookmark for the same url, tags should be moved.
+ observer.reset();
+ await PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, false);
+
+ observer.reset();
+ await PT.redo();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, false);
+
+ // When there're multiple bookmarks for the same url, tags should be copied.
+ let bm2_info = Object.create(bm_info);
+ bm2_info.guid = await PT.NewBookmark(bm2_info).transact();
+ let bm3_info = Object.create(bm_info);
+ bm3_info.url = newURI;
+ bm3_info.guid = await PT.NewBookmark(bm3_info).transact();
+
+ observer.reset();
+ await PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+
+ observer.reset();
+ await PT.redo();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+ await PT.undo();
+ await PT.undo();
+ await PT.undo();
+ ensureItemsRemoved(bm3_info, bm2_info, bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_keyword() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test.edit.keyword/",
+ };
+ const KEYWORD = "test_keyword";
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "") {
+ ensureItemsChanged({
+ guid: bm_info.guid,
+ property: "keyword",
+ newValue: aCurrentKeyword,
+ });
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditKeyword({
+ guid: bm_info.guid,
+ keyword: KEYWORD,
+ postData: "postData",
+ }).transact();
+ ensureKeywordChange(KEYWORD);
+ let entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData");
+
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.redo();
+ ensureKeywordChange(KEYWORD);
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData");
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_keyword_null_postData() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test.edit.keyword/",
+ };
+ const KEYWORD = "test_keyword";
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "") {
+ ensureItemsChanged({
+ guid: bm_info.guid,
+ property: "keyword",
+ newValue: aCurrentKeyword,
+ });
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditKeyword({
+ guid: bm_info.guid,
+ keyword: KEYWORD,
+ postData: null,
+ }).transact();
+ ensureKeywordChange(KEYWORD);
+ let entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, null);
+
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.redo();
+ ensureKeywordChange(KEYWORD);
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, null);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_specific_keyword() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test.edit.keyword/",
+ };
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") {
+ ensureItemsChanged({
+ guid: bm_info.guid,
+ property: "keyword",
+ newValue: aCurrentKeyword,
+ });
+ }
+
+ await PlacesUtils.keywords.insert({
+ keyword: "kw1",
+ url: bm_info.url,
+ postData: "postData1",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "kw2",
+ url: bm_info.url,
+ postData: "postData2",
+ });
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditKeyword({
+ guid: bm_info.guid,
+ keyword: "keyword",
+ oldKeyword: "kw2",
+ }).transact();
+ ensureKeywordChange("keyword", "kw2");
+ let entry = await PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData1");
+ entry = await PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData2");
+ entry = await PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange("kw2", "keyword");
+ entry = await PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData1");
+ entry = await PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData2");
+ entry = await PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.redo();
+ ensureKeywordChange("keyword", "kw2");
+ entry = await PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData1");
+ entry = await PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData2");
+ entry = await PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange("kw2");
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_tag_uri() {
+ // This also tests passing uri specs.
+ let bm_info_a = {
+ url: "http://bookmarked.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ let bm_info_b = {
+ url: "http://bookmarked2.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ let unbookmarked_uri = "http://un.bookmarked.uri";
+
+ await PT.batch(async function() {
+ bm_info_a.guid = await PT.NewBookmark(bm_info_a).transact();
+ bm_info_b.guid = await PT.NewBookmark(bm_info_b).transact();
+ });
+
+ async function doTest(aInfo) {
+ let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
+ let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
+
+ let tagWillAlsoBookmark = new Set();
+ for (let url of urls) {
+ if (!(await bmsvc.fetch({ url }))) {
+ tagWillAlsoBookmark.add(url);
+ }
+ }
+
+ async function ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, tags);
+ Assert.ok(await bmsvc.fetch({ url }));
+ }
+ }
+ async function ensureTagsUnset() {
+ for (let url of urls) {
+ ensureTagsForURI(url, []);
+ if (tagWillAlsoBookmark.has(url)) {
+ Assert.ok(!(await bmsvc.fetch({ url })));
+ } else {
+ Assert.ok(await bmsvc.fetch({ url }));
+ }
+ }
+ }
+
+ await PT.Tag(aInfo).transact();
+ await ensureTagsSet();
+ await PT.undo();
+ await ensureTagsUnset();
+ await PT.redo();
+ await ensureTagsSet();
+ await PT.undo();
+ await ensureTagsUnset();
+ }
+
+ await doTest({ url: bm_info_a.url, tags: ["MyTag"] });
+ await doTest({ urls: [bm_info_a.url], tag: "MyTag" });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] });
+ await doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" });
+ // Duplicate URLs listed.
+ await doTest({
+ urls: [bm_info_a.url, bm_info_b.url, bm_info_a.url],
+ tag: "D",
+ });
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_untag_uri() {
+ let bm_info_a = {
+ url: "http://bookmarked.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ tags: ["A", "B"],
+ };
+ let bm_info_b = {
+ url: "http://bookmarked2.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ tag: "B",
+ };
+
+ await PT.batch(async function() {
+ bm_info_a.guid = await PT.NewBookmark(bm_info_a).transact();
+ ensureTagsForURI(bm_info_a.url, bm_info_a.tags);
+ bm_info_b.guid = await PT.NewBookmark(bm_info_b).transact();
+ ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]);
+ });
+
+ async function doTest(aInfo) {
+ let urls, tagsRemoved;
+ if (typeof aInfo == "string") {
+ urls = [aInfo];
+ tagsRemoved = [];
+ } else if (Array.isArray(aInfo)) {
+ urls = aInfo;
+ tagsRemoved = [];
+ } else {
+ urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
+ tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
+ }
+
+ let preRemovalTags = new Map();
+ for (let url of urls) {
+ preRemovalTags.set(url, tagssvc.getTagsForURI(Services.io.newURI(url)));
+ }
+
+ function ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, preRemovalTags.get(url));
+ }
+ }
+ function ensureTagsUnset() {
+ for (let url of urls) {
+ let expectedTags = !tagsRemoved.length
+ ? []
+ : preRemovalTags.get(url).filter(tag => !tagsRemoved.includes(tag));
+ ensureTagsForURI(url, expectedTags);
+ }
+ }
+
+ await PT.Untag(aInfo).transact();
+ await ensureTagsUnset();
+ await PT.undo();
+ await ensureTagsSet();
+ await PT.redo();
+ await ensureTagsUnset();
+ await PT.undo();
+ await ensureTagsSet();
+ }
+
+ await doTest(bm_info_a);
+ await doTest(bm_info_b);
+ await doTest(bm_info_a.url);
+ await doTest(bm_info_b.url);
+ await doTest([bm_info_a.url, bm_info_b.url]);
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] });
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_sort_folder_by_name() {
+ let folder_info = createTestFolderInfo();
+
+ let url = "http://sort.by.name/";
+ let preSep = ["3", "2", "1"].map(i => ({ title: i, url }));
+ let sep = {};
+ let postSep = ["c", "b", "a"].map(l => ({ title: l, url }));
+ let originalOrder = [...preSep, sep, ...postSep];
+ let sortedOrder = [
+ ...preSep.slice(0).reverse(),
+ sep,
+ ...postSep.slice(0).reverse(),
+ ];
+ await PT.batch(async function() {
+ folder_info.guid = await PT.NewFolder(folder_info).transact();
+ for (let info of originalOrder) {
+ info.parentGuid = folder_info.guid;
+ info.guid = await (info == sep
+ ? PT.NewSeparator(info).transact()
+ : PT.NewBookmark(info).transact());
+ }
+ });
+
+ let folderContainer = PlacesUtils.getFolderContents(folder_info.guid).root;
+ function ensureOrder(aOrder) {
+ for (let i = 0; i < folderContainer.childCount; i++) {
+ Assert.equal(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid);
+ }
+ }
+
+ ensureOrder(originalOrder);
+ await PT.SortByName(folder_info.guid).transact();
+ ensureOrder(sortedOrder);
+ await PT.undo();
+ ensureOrder(originalOrder);
+ await PT.redo();
+ ensureOrder(sortedOrder);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureOrder(originalOrder);
+ await PT.undo();
+ ensureItemsRemoved(...originalOrder, folder_info);
+});
+
+add_task(async function test_copy() {
+ async function duplicate_and_test(aOriginalGuid) {
+ let txn = PT.Copy({
+ guid: aOriginalGuid,
+ newParentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ let duplicateGuid = await txn.transact();
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(aOriginalGuid);
+ let duplicateInfo = await PlacesUtils.promiseBookmarksTree(duplicateGuid);
+ await ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false);
+
+ async function redo() {
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo);
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(duplicateInfo);
+ }
+ async function undo() {
+ await PT.undo();
+ // also undo the original item addition.
+ await PT.undo();
+ await ensureNonExistent(aOriginalGuid, duplicateGuid);
+ }
+
+ await undo();
+ await redo();
+ await undo();
+ await redo();
+
+ // Cleanup. This also remove the original item.
+ await PT.undo();
+ observer.reset();
+ await PT.clearTransactionsHistory();
+ }
+
+ let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
+ Preferences.set("privacy.reduceTimerPrecision", false);
+
+ registerCleanupFunction(function() {
+ Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
+ });
+
+ // Test duplicating leafs (bookmark, separator, empty folder)
+ PT.NewBookmark({
+ url: "http://test.item.duplicate",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ annos: [{ name: "Anno", value: "AnnoValue" }],
+ });
+ let sepTxn = PT.NewSeparator({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 1,
+ });
+
+ let emptyFolderTxn = PT.NewFolder(createTestFolderInfo());
+ for (let txn of [sepTxn, emptyFolderTxn]) {
+ let guid = await txn.transact();
+ await duplicate_and_test(guid);
+ }
+
+ // Test duplicating a folder having some contents.
+ let filledFolderGuid = await PT.batch(async function() {
+ let folderGuid = await PT.NewFolder(createTestFolderInfo()).transact();
+ let nestedFolderGuid = await PT.NewFolder({
+ parentGuid: folderGuid,
+ title: "Nested Folder",
+ }).transact();
+ // Insert a bookmark under the nested folder.
+ await PT.NewBookmark({
+ url: "http://nested.nested.bookmark",
+ parentGuid: nestedFolderGuid,
+ }).transact();
+ // Insert a separator below the nested folder
+ await PT.NewSeparator({ parentGuid: folderGuid }).transact();
+ // And another bookmark.
+ await PT.NewBookmark({
+ url: "http://nested.bookmark",
+ parentGuid: folderGuid,
+ }).transact();
+ return folderGuid;
+ });
+
+ await duplicate_and_test(filledFolderGuid);
+
+ // Cleanup
+ await PT.clearTransactionsHistory();
+});
+
+add_task(async function test_array_input_for_batch() {
+ let folderTxn = PT.NewFolder(createTestFolderInfo());
+ let folderGuid = await folderTxn.transact();
+
+ let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ await PT.batch([sep1_txn, sep2_txn]);
+ ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0);
+
+ let ensureChildCount = async function(count) {
+ let tree = await PlacesUtils.promiseBookmarksTree(folderGuid);
+ if (count == 0) {
+ Assert.ok(!("children" in tree));
+ } else {
+ Assert.equal(tree.children.length, count);
+ }
+ };
+
+ await ensureChildCount(2);
+ await PT.undo();
+ await ensureChildCount(0);
+ await PT.redo();
+ await ensureChildCount(2);
+ await PT.undo();
+ await ensureChildCount(0);
+
+ await PT.undo();
+ Assert.equal(await PlacesUtils.promiseBookmarksTree(folderGuid), null);
+
+ // Cleanup
+ await PT.clearTransactionsHistory();
+});
+
+add_task(async function test_invalid_uri_spec_throws() {
+ Assert.throws(
+ () =>
+ PT.NewBookmark({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "invalid uri spec",
+ title: "test bookmark",
+ }),
+ /invalid uri spec is not a valid URL/
+ );
+ Assert.throws(
+ () => PT.Tag({ tag: "TheTag", urls: ["invalid uri spec"] }),
+ /TypeError: URL constructor: invalid uri spec is not a valid URL/
+ );
+ Assert.throws(
+ () => PT.Tag({ tag: "TheTag", urls: ["about:blank", "invalid uri spec"] }),
+ /TypeError: URL constructor: invalid uri spec is not a valid URL/
+ );
+});
+
+add_task(async function test_remove_multiple() {
+ let guids = [];
+ await PT.batch(async function() {
+ let folderGuid = await PT.NewFolder({
+ title: "Test Folder",
+ parentGuid: menuGuid,
+ }).transact();
+ let nestedFolderGuid = await PT.NewFolder({
+ title: "Nested Test Folder",
+ parentGuid: folderGuid,
+ }).transact();
+ await PT.NewSeparator(nestedFolderGuid).transact();
+
+ guids.push(folderGuid);
+
+ let bmGuid = await PT.NewBookmark({
+ url: "http://test.bookmark.removed",
+ parentGuid: menuGuid,
+ }).transact();
+ guids.push(bmGuid);
+ });
+
+ let originalInfos = [];
+ for (let guid of guids) {
+ originalInfos.push(await PlacesUtils.promiseBookmarksTree(guid));
+ }
+
+ await PT.Remove(guids).transact();
+ await ensureNonExistent(...guids);
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+ await PT.redo();
+ await ensureNonExistent(...guids);
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Undo the New* transactions batch.
+ await PT.undo();
+ await ensureNonExistent(...guids);
+
+ // Redo it.
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(...originalInfos);
+
+ // Redo remove.
+ await PT.redo();
+ await ensureNonExistent(...guids);
+
+ // Cleanup
+ await PT.clearTransactionsHistory();
+ observer.reset();
+});
+
+add_task(async function test_renameTag() {
+ let url = "http://test.edit.keyword/";
+ await PT.Tag({ url, tags: ["t1", "t2"] }).transact();
+ ensureTagsForURI(url, ["t1", "t2"]);
+
+ // Create bookmark queries that point to the modified tag.
+ let bm1 = await PlacesUtils.bookmarks.insert({
+ url: "place:tag=t2",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ let bm2 = await PlacesUtils.bookmarks.insert({
+ url: "place:tag=t2&sort=1",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ // This points to 2 tags, and as such won't be touched.
+ let bm3 = await PlacesUtils.bookmarks.insert({
+ url: "place:tag=t2&tag=t1",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ await PT.RenameTag({ oldTag: "t2", tag: "t3" }).transact();
+ ensureTagsForURI(url, ["t1", "t3"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t3",
+ "The fitst bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t3&sort=1",
+ "The second bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t3&tag=t1",
+ "The third bookmark has been updated"
+ );
+
+ await PT.undo();
+ ensureTagsForURI(url, ["t1", "t2"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t2",
+ "The fitst bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t2&sort=1",
+ "The second bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t2&tag=t1",
+ "The third bookmark has been restored"
+ );
+
+ await PT.redo();
+ ensureTagsForURI(url, ["t1", "t3"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t3",
+ "The fitst bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t3&sort=1",
+ "The second bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t3&tag=t1",
+ "The third bookmark has been updated"
+ );
+
+ await PT.undo();
+ ensureTagsForURI(url, ["t1", "t2"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t2",
+ "The fitst bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t2&sort=1",
+ "The second bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t2&tag=t1",
+ "The third bookmark has been restored"
+ );
+
+ await PT.undo();
+ ensureTagsForURI(url, []);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_remove_invalid_url() {
+ let folderGuid = await PT.NewFolder({
+ title: "Test Folder",
+ parentGuid: menuGuid,
+ }).transact();
+
+ let guid = "invalid_____";
+ let folderedGuid = "invalid____2";
+ let url = "invalid-uri";
+ await PlacesUtils.withConnectionWrapper("test_bookmarks_remove", async db => {
+ await db.execute(
+ `
+ INSERT INTO moz_places(url, url_hash, title, rev_host, guid)
+ VALUES (:url, hash(:url), 'Invalid URI', '.', GENERATE_GUID())
+ `,
+ { url }
+ );
+ await db.execute(
+ `INSERT INTO moz_bookmarks (type, fk, parent, position, guid)
+ VALUES (:type,
+ (SELECT id FROM moz_places WHERE url = :url),
+ (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid),
+ (SELECT MAX(position) + 1 FROM moz_bookmarks WHERE parent = (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid)),
+ :guid)
+ `,
+ {
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ guid,
+ }
+ );
+ await db.execute(
+ `INSERT INTO moz_bookmarks (type, fk, parent, position, guid)
+ VALUES (:type,
+ (SELECT id FROM moz_places WHERE url = :url),
+ (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid),
+ (SELECT MAX(position) + 1 FROM moz_bookmarks WHERE parent = (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid)),
+ :guid)
+ `,
+ {
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url,
+ parentGuid: folderGuid,
+ guid: folderedGuid,
+ }
+ );
+ });
+
+ let guids = [folderGuid, guid];
+ await PT.Remove(guids).transact();
+ await ensureNonExistent(...guids, folderedGuid);
+ // Shouldn't throw, should restore the folder but not the bookmarks.
+ await PT.undo();
+ await ensureNonExistent(guid, folderedGuid);
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch(folderGuid),
+ "The folder should have been re-created"
+ );
+ await PT.redo();
+ await ensureNonExistent(guids, folderedGuid);
+ // Cleanup
+ await PT.clearTransactionsHistory();
+ observer.reset();
+});