fune/toolkit/components/places/tests/unit/test_async_transactions.js
Doug Thayer 0fd7f560d6 Bug 1426245 - Test changes r=mak
MozReview-Commit-ID: 4fhhzspxLJZ

Depends on D4606

Differential Revision: https://phabricator.services.mozilla.com/D5162

--HG--
extra : moz-landing-system : lando
2018-10-09 14:47:31 +00:00

1931 lines
63 KiB
JavaScript

/* -*- 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 annosvc = PlacesUtils.annotations;
const PT = PlacesTransactions;
const menuGuid = PlacesUtils.bookmarks.menuGuid;
Cu.importGlobalProperties(["URL"]);
ChromeUtils.defineModuleGetter(this, "Preferences",
"resource://gre/modules/Preferences.jsm");
// Create and add bookmarks observer.
var observer = {
__proto__: NavBookmarkObserver.prototype,
tagRelatedGuids: new Set(),
reset() {
this.itemsAdded = new Map();
this.itemsRemoved = new Map();
this.itemsChanged = new Map();
this.itemsMoved = new Map();
this.beginUpdateBatch = false;
this.endUpdateBatch = false;
},
handlePlacesEvents(events) {
for (let event of events) {
// 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 });
}
},
onBeginUpdateBatch() {
this.beginUpdateBatch = true;
},
onEndUpdateBatch() {
this.endUpdateBatch = true;
},
onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGuid, aParentGuid) {
if (this.tagRelatedGuids.has(aGuid))
return;
this.itemsRemoved.set(aGuid, { parentGuid: aParentGuid,
index: aIndex,
itemType: aItemType });
},
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 newValue = aNewValue;
if (aIsAnnoProperty) {
if (annosvc.itemHasAnnotation(aItemId, aProperty))
newValue = annosvc.getItemAnnotation(aItemId, aProperty);
else
newValue = null;
}
let change = { isAnnoProperty: aIsAnnoProperty,
newValue,
lastModified: aLastModified,
itemType: aItemType };
changesForGuid.set(aProperty, change);
},
onItemVisited: () => {},
onItemMoved(aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex, aItemType,
aGuid, aOldParentGuid, aNewParentGuid) {
this.itemsMoved.set(aGuid, { oldParentGuid: aOldParentGuid,
oldIndex: aOldIndex,
newParentGuid: aNewParentGuid,
newIndex: aNewIndex,
itemType: aItemType });
},
};
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"], observer.handlePlacesEvents);
registerCleanupFunction(function() {
bmsvc.removeObserver(observer);
obsvc.removeListener(["bookmark-added"], 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);
if (!("isAnnoProperty" in item)) {
Assert.ok(!info.isAnnoProperty);
} else {
Assert.equal(info.isAnnoProperty, Boolean(item.isAnnoProperty));
}
Assert.equal(info.newValue, item.newValue);
if ("url" in item)
Assert.ok(item.url.equals(info.url));
}
}
function ensureAnnotationsSet(aGuid, aAnnos) {
Assert.ok(observer.itemsChanged.has(aGuid));
let changes = observer.itemsChanged.get(aGuid);
for (let anno of aAnnos) {
Assert.ok(changes.has(anno.name));
let changeInfo = changes.get(anno.name);
Assert.ok(changeInfo.isAnnoProperty);
Assert.equal(changeInfo.newValue, anno.value);
}
}
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 ensureTimestampsUpdated(aGuid, aCheckDateAdded = false) {
Assert.ok(observer.itemsChanged.has(aGuid));
let changes = observer.itemsChanged.get(aGuid);
if (aCheckDateAdded)
Assert.ok(changes.has("dateAdded"));
Assert.ok(changes.has("lastModified"));
}
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 isLivemarkTree(aTree) {
return !!aTree.annos &&
aTree.annos.some( a => a.name == PlacesUtils.LMANNO_FEEDURI );
}
async function ensureLivemarkCreatedByAddLivemark(aLivemarkGuid) {
// This throws otherwise.
await PlacesUtils.livemarks.getLivemark({ guid: aLivemarkGuid });
}
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);
if (isLivemarkTree(aNew))
await ensureLivemarkCreatedByAddLivemark(aNew.guid);
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]);
}
}
if (isLivemarkTree(aNew))
await ensureLivemarkCreatedByAddLivemark(aNew.guid);
}
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_annotation() {
const ANNO = { name: "TestAnno", value: "TestValue" };
let folder_info = createTestFolderInfo();
folder_info.index = bmStartIndex;
folder_info.annotations = [ANNO];
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);
await ensureItemsAdded(folder_info);
ensureAnnotationsSet(folder_info.guid, [ANNO]);
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 });
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_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";
const ANNO = { name: "TestAnno", value: "TestAnnoValue" };
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],
annotations: [ANNO] };
b2_info.guid = await PT.NewBookmark(b2_info).transact();
let b2_post_creation_changes = [
{ guid: b2_info.guid,
isAnnoProperty: true,
property: ANNO.name,
newValue: ANNO.value } ];
ensureItemsChanged(...b2_post_creation_changes);
ensureTags([TAG_1, TAG_2]);
observer.reset();
await PT.undo();
await ensureItemsRemoved(b2_info);
ensureTags([TAG_1]);
// Check if Remove correctly restores tags and annotations.
observer.reset();
await PT.redo();
ensureItemsChanged({ guid: b2_info.guid,
isAnnoProperty: true,
property: ANNO.name,
newValue: ANNO.value });
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();
ensureItemsChanged(...b2_post_creation_changes);
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();
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_add_and_remove_livemark() {
let createLivemarkTxn = PT.NewLivemark(
{ feedUrl: "http://test.remove.livemark",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
title: "Test Remove Livemark" });
let guid = await createLivemarkTxn.transact();
let originalInfo = await PlacesUtils.promiseBookmarksTree(guid);
Assert.ok(originalInfo);
await ensureLivemarkCreatedByAddLivemark(guid);
let removeTxn = PT.Remove(guid);
await removeTxn.transact();
await ensureNonExistent(guid);
async function undo() {
ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
await PT.undo();
ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
await ensureBookmarksTreeRestoredCorrectly(originalInfo);
await PT.undo();
ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
await ensureNonExistent(guid);
}
async function redo() {
ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
await PT.redo();
ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
await ensureBookmarksTreeRestoredCorrectly(originalInfo);
await PT.redo();
ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
await ensureNonExistent(guid);
}
await undo();
await redo();
await undo();
await redo();
// Cleanup
await undo();
observer.reset();
await PT.clearTransactionsHistory();
});
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) {
ensureItemsChanged({ guid: bm_info.guid,
property: "title",
newValue: aCurrentTitle});
}
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) {
ensureItemsChanged({ guid: bm_info.guid,
property: "uri",
newValue: 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 == 0 ?
[] : 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_livemark_txns() {
let livemark_info =
{ feedUrl: "http://test.feed.uri/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
title: "Test Livemark" };
function ensureLivemarkAdded() {
ensureItemsAdded({ guid: livemark_info.guid,
title: livemark_info.title,
parentGuid: livemark_info.parentGuid,
itemType: PlacesUtils.bookmarks.TYPE_FOLDER });
let annos = [{ name: PlacesUtils.LMANNO_FEEDURI,
value: livemark_info.feedUrl }];
if ("siteUrl" in livemark_info) {
annos.push({ name: PlacesUtils.LMANNO_SITEURI,
value: livemark_info.siteUrl });
}
ensureAnnotationsSet(livemark_info.guid, annos);
}
function ensureLivemarkRemoved() {
ensureItemsRemoved({ guid: livemark_info.guid,
parentGuid: livemark_info.parentGuid });
}
async function _testDoUndoRedoUndo() {
observer.reset();
livemark_info.guid = await PT.NewLivemark(livemark_info).transact();
ensureLivemarkAdded();
observer.reset();
await PT.undo();
ensureLivemarkRemoved();
observer.reset();
await PT.redo();
ensureLivemarkAdded();
await PT.undo();
ensureLivemarkRemoved();
}
await _testDoUndoRedoUndo();
livemark_info.siteUrl = "http://feed.site.uri/";
await _testDoUndoRedoUndo();
// Cleanup
observer.reset();
await PT.clearTransactionsHistory();
});
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 livemarkTxn = PT.NewLivemark(
{ feedUrl: "http://test.feed.uri",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
title: "Test Livemark", index: 1 });
let emptyFolderTxn = PT.NewFolder(createTestFolderInfo());
for (let txn of [livemarkTxn, 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_copy_excluding_annotations() {
let folderInfo = createTestFolderInfo();
let anno = n => { return { name: n, value: 1 }; };
folderInfo.annotations = [anno("a"), anno("b"), anno("c")];
let folderGuid = await PT.NewFolder(folderInfo).transact();
let ensureAnnosSet = async function(guid, ...expectedAnnoNames) {
let tree = await PlacesUtils.promiseBookmarksTree(guid);
let annoNames = "annos" in tree ?
tree.annos.map(a => a.name).sort() : [];
Assert.deepEqual(annoNames, expectedAnnoNames);
};
await ensureAnnosSet(folderGuid, "a", "b", "c");
let excluding_a_dupeGuid =
await PT.Copy({ guid: folderGuid,
newParentGuid: PlacesUtils.bookmarks.unfiledGuid,
excludingAnnotation: "a" }).transact();
await ensureAnnosSet(excluding_a_dupeGuid, "b", "c");
let excluding_ac_dupeGuid =
await PT.Copy({ guid: folderGuid,
newParentGuid: PlacesUtils.bookmarks.unfiledGuid,
excludingAnnotations: ["a", "c"] }).transact();
await ensureAnnosSet(excluding_ac_dupeGuid, "b");
// Cleanup
await PT.undo();
await PT.undo();
await PT.undo();
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: invalid uri spec is not a valid URL/);
Assert.throws(() =>
PT.Tag({ tag: "TheTag",
urls: ["about:blank", "invalid uri spec"] }),
/TypeError: 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();
});