Bug 1150678 - Changing url of a bookmark with a keyword breaks the keyword forever. r=adw

MozReview-Commit-ID: 89Od4PKpQse

--HG--
extra : rebase_source : 4fc77a9e4c2241787041a57da10907f32250babd
This commit is contained in:
Marco Bonardo 2016-08-30 11:04:22 +02:00
parent ac2f0b721a
commit d399b52d03
11 changed files with 336 additions and 76 deletions

View File

@ -131,14 +131,18 @@ var gEditItemOverlay = {
PlacesUIUtils.getItemDescription(this._paneInfo.itemId));
},
_initKeywordField: Task.async(function* (aNewKeyword) {
if (!this._paneInfo.isBookmark)
_initKeywordField: Task.async(function* (newKeyword = "") {
if (!this._paneInfo.isBookmark) {
throw new Error("_initKeywordField called unexpectedly");
}
let newKeyword = aNewKeyword;
if (newKeyword === undefined) {
let itemId = this._paneInfo.itemId;
newKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
if (!newKeyword) {
let entries = [];
yield PlacesUtils.keywords.fetch({ url: this._paneInfo.uri.spec },
e => entries.push(e));
if (entries.length > 0) {
this._keyword = newKeyword = entries[0].keyword;
}
}
this._initTextField(this._keywordField, newKeyword);
}),
@ -215,7 +219,7 @@ var gEditItemOverlay = {
}
if (showOrCollapse("keywordRow", isBookmark, "keyword")) {
this._initKeywordField();
this._initKeywordField().catch(Components.utils.reportError);
this._keywordField.readOnly = this.readOnly;
}
@ -568,7 +572,7 @@ var gEditItemOverlay = {
: this._paneInfo.itemGuid;
PlacesTransactions.EditTitle({ guid, title: newTitle })
.transact().catch(Components.utils.reportError);
}).catch(Cu.reportError);
}).catch(Components.utils.reportError);
}
},
@ -625,14 +629,19 @@ var gEditItemOverlay = {
return;
let itemId = this._paneInfo.itemId;
let newKeyword = this._keywordField.value;
let oldKeyword = this._keyword;
let keyword = this._keyword = this._keywordField.value;
let postData = this._paneInfo.postData;
if (!PlacesUIUtils.useAsyncTransactions) {
let txn = new PlacesEditBookmarkKeywordTransaction(itemId, newKeyword, this._paneInfo.postData);
let txn = new PlacesEditBookmarkKeywordTransaction(itemId,
keyword,
postData,
oldKeyword);
PlacesUtils.transactionManager.doTransaction(txn);
return;
}
let guid = this._paneInfo.itemGuid;
PlacesTransactions.EditKeyword({ guid, keyword: newKeyword })
PlacesTransactions.EditKeyword({ guid, keyword, postData, oldKeyword })
.transact().catch(Components.utils.reportError);
},
@ -1085,7 +1094,7 @@ var gEditItemOverlay = {
break;
case "keyword":
if (this._paneInfo.visibleRows.has("keywordRow"))
this._initKeywordField(aValue);
this._initKeywordField(aValue).catch(Components.utils.reportError);
break;
case PlacesUIUtils.DESCRIPTION_ANNO:
if (this._paneInfo.visibleRows.has("descriptionRow"))

View File

@ -854,6 +854,13 @@ Database::InitSchema(bool* aDatabaseMigrated)
// Firefox 50 uses schema version 33.
if (currentSchemaVersion < 34) {
rv = MigrateV34Up();
NS_ENSURE_SUCCESS(rv, rv);
}
// Firefox 51 uses schema version 34.
// Schema Upgrades must add migration code here.
rv = UpdateBookmarkRootTitles();
@ -1813,6 +1820,21 @@ Database::MigrateV33Up() {
return NS_OK;
}
nsresult
Database::MigrateV34Up() {
MOZ_ASSERT(NS_IsMainThread());
nsresult rv = mMainConn->ExecuteSimpleSQL(NS_LITERAL_CSTRING(
"DELETE FROM moz_keywords WHERE id IN ( "
"SELECT id FROM moz_keywords k "
"WHERE NOT EXISTS (SELECT 1 FROM moz_places h WHERE k.place_id = h.id) "
")"
));
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
void
Database::Shutdown()
{

View File

@ -18,7 +18,7 @@
// This is the schema version. Update it at any schema change and add a
// corresponding migrateVxx method below.
#define DATABASE_SCHEMA_VERSION 33
#define DATABASE_SCHEMA_VERSION 34
// Fired after Places inited.
#define TOPIC_PLACES_INIT_COMPLETE "places-init-complete"
@ -269,6 +269,7 @@ protected:
nsresult MigrateV31Up();
nsresult MigrateV32Up();
nsresult MigrateV33Up();
nsresult MigrateV34Up();
nsresult UpdateBookmarkRootTitles();

View File

@ -885,7 +885,7 @@ DefineTransaction.defineInputProps(["guid", "parentGuid", "newParentGuid"],
DefineTransaction.guidValidate);
DefineTransaction.defineInputProps(["title"],
DefineTransaction.strOrNullValidate, null);
DefineTransaction.defineInputProps(["keyword", "postData", "tag",
DefineTransaction.defineInputProps(["keyword", "oldKeyword", "postData", "tag",
"excludingAnnotation"],
DefineTransaction.strValidate, "");
DefineTransaction.defineInputProps(["index", "newIndex"],
@ -1000,8 +1000,12 @@ function* createItemsFromBookmarksTree(aBookmarksTree, aRestoring = false,
let uri = NetUtil.newURI(aItem.uri);
itemId = PlacesUtils.bookmarks.insertBookmark(
parentId, uri, aIndex, aItem.title, guid);
if ("keyword" in aItem)
PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aItem.keyword);
if ("keyword" in aItem) {
yield PlacesUtils.keywords.insert({
keyword: aItem.keyword,
url: uri.spec
});
}
if ("tags" in aItem) {
PlacesUtils.tagging.tagURI(uri, aItem.tags.split(","));
}
@ -1084,15 +1088,20 @@ PT.NewBookmark.prototype = Object.seal({
execute: function (aParentGuid, aURI, aIndex, aTitle,
aKeyword, aPostData, aAnnos, aTags) {
return ExecuteCreateItem(this, aParentGuid,
function (parentId, guidToRestore = "") {
function* (parentId, guidToRestore = "") {
let itemId = PlacesUtils.bookmarks.insertBookmark(
parentId, aURI, aIndex, aTitle, guidToRestore);
if (aKeyword)
PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
if (aPostData)
PlacesUtils.setPostDataForBookmark(itemId, aPostData);
if (aAnnos.length)
if (aKeyword) {
yield PlacesUtils.keywords.insert({
url: aURI.spec,
keyword: aKeyword,
postData: aPostData
});
}
if (aAnnos.length) {
PlacesUtils.setAnnotationsForItem(itemId, aAnnos);
}
if (aTags.length > 0) {
let currentTags = PlacesUtils.tagging.getTagsForURI(aURI);
aTags = aTags.filter(t => !currentTags.includes(t));
@ -1102,8 +1111,9 @@ PT.NewBookmark.prototype = Object.seal({
return itemId;
},
function _additionalOnUndo() {
if (aTags.length > 0)
if (aTags.length > 0) {
PlacesUtils.tagging.untagURI(aURI, aTags);
}
});
}
});
@ -1121,7 +1131,7 @@ PT.NewFolder = DefineTransaction(["parentGuid", "title"],
PT.NewFolder.prototype = Object.seal({
execute: function (aParentGuid, aTitle, aIndex, aAnnos) {
return ExecuteCreateItem(this, aParentGuid,
function(parentId, guidToRestore = "") {
function* (parentId, guidToRestore = "") {
let itemId = PlacesUtils.bookmarks.createFolder(
parentId, aTitle, aIndex, guidToRestore);
if (aAnnos.length > 0)
@ -1144,7 +1154,7 @@ PT.NewSeparator = DefineTransaction(["parentGuid"], ["index"]);
PT.NewSeparator.prototype = Object.seal({
execute: function (aParentGuid, aIndex) {
return ExecuteCreateItem(this, aParentGuid,
function (parentId, guidToRestore = "") {
function* (parentId, guidToRestore = "") {
let itemId = PlacesUtils.bookmarks.insertSeparator(
parentId, aIndex, guidToRestore);
return itemId;
@ -1336,14 +1346,36 @@ PT.Annotate.prototype = {
*
* Required Input Properties: guid, keyword.
*/
PT.EditKeyword = DefineTransaction(["guid", "keyword"]);
PT.EditKeyword = DefineTransaction(["guid", "keyword"],
["postData", "oldKeyword"]);
PT.EditKeyword.prototype = Object.seal({
execute: function* (aGuid, aKeyword) {
let itemId = yield PlacesUtils.promiseItemId(aGuid),
oldKeyword = PlacesUtils.bookmarks.getKeywordForBookmark(itemId);
PlacesUtils.bookmarks.setKeywordForBookmark(itemId, aKeyword);
this.undo = () => {
PlacesUtils.bookmarks.setKeywordForBookmark(itemId, oldKeyword);
execute: function* (aGuid, aKeyword, aPostData, aOldKeyword) {
let url;
let oldKeywordEntry;
if (aOldKeyword) {
oldKeywordEntry = yield PlacesUtils.keywords.fetch(aOldKeyword);
url = oldKeywordEntry.url;
yield PlacesUtils.keywords.remove(aOldKeyword);
}
if (aKeyword) {
if (!url) {
url = (yield PlacesUtils.bookmarks.fetch(aGuid)).url;
}
yield PlacesUtils.keywords.insert({
url: url,
keyword: aKeyword,
postData: aPostData || (oldKeywordEntry ? oldKeywordEntry.postData : "")
});
}
this.undo = function* () {
if (aKeyword) {
yield PlacesUtils.keywords.remove(aKeyword);
}
if (oldKeywordEntry) {
yield PlacesUtils.keywords.insert(oldKeywordEntry);
}
};
}
});

View File

@ -1827,7 +1827,8 @@ this.PlacesUtils = {
}.bind(this);
const QUERY_STR =
`WITH RECURSIVE
`/* do not warn (bug no): cannot use an index */
WITH RECURSIVE
descendants(fk, level, type, id, guid, parent, parentGuid, position,
title, dateAdded, lastModified) AS (
SELECT b1.fk, 0, b1.type, b1.id, b1.guid, b1.parent,
@ -2356,29 +2357,54 @@ XPCOMUtils.defineLazyGetter(this, "gKeywordsCachePromise", () =>
}).catch(Cu.reportError);
},
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid) {
if (gIgnoreKeywordNotifications ||
prop != "keyword")
onItemChanged(id, prop, isAnno, val, lastMod, itemType, parentId, guid,
parentGuid, oldVal) {
if (gIgnoreKeywordNotifications) {
return;
}
Task.spawn(function* () {
let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
// By this time the bookmark could have gone, there's nothing we can do.
if (!bookmark)
return;
if (prop == "keyword") {
this._onKeywordChanged(guid, val).catch(Cu.reportError);
} else if (prop == "uri") {
this._onUrlChanged(guid, val, oldVal).catch(Cu.reportError);
}
},
if (val.length == 0) {
// We are removing a keyword.
let keywords = keywordsForHref(bookmark.url.href)
for (let keyword of keywords) {
cache.delete(keyword);
}
} else {
// We are adding a new keyword.
cache.set(val, { keyword: val, url: bookmark.url });
_onKeywordChanged: Task.async(function* (guid, keyword) {
let bookmark = yield PlacesUtils.bookmarks.fetch(guid);
// Due to mixed sync/async operations, by this time the bookmark could
// have disappeared and we already handle removals in onItemRemoved.
if (!bookmark) {
return;
}
if (keyword.length == 0) {
// We are removing a keyword.
let keywords = keywordsForHref(bookmark.url.href)
for (let kw of keywords) {
cache.delete(kw);
}
}).catch(Cu.reportError);
}
} else {
// We are adding a new keyword.
cache.set(keyword, { keyword, url: bookmark.url });
}
}),
_onUrlChanged: Task.async(function* (guid, url, oldUrl) {
// Check if the old url is associated with keywords.
let entries = [];
yield PlacesUtils.keywords.fetch({ url: oldUrl }, e => entries.push(e));
if (entries.length == 0) {
return;
}
// Move the keywords to the new url.
for (let entry of entries) {
yield PlacesUtils.keywords.remove(entry.keyword);
entry.url = new URL(url);
yield PlacesUtils.keywords.insert(entry);
}
}),
};
PlacesUtils.bookmarks.addObserver(observer, false);
@ -3423,14 +3449,18 @@ PlacesSetPageAnnotationTransaction.prototype = {
* new keyword for the bookmark
* @param aNewPostData [optional]
* new keyword's POST data, if available
* @param aOldKeyword [optional]
* old keyword of the bookmark
*
* @return nsITransaction object
*/
this.PlacesEditBookmarkKeywordTransaction =
function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword, aNewPostData)
{
function PlacesEditBookmarkKeywordTransaction(aItemId, aNewKeyword,
aNewPostData, aOldKeyword) {
this.item = new TransactionItemCache();
this.item.id = aItemId;
this.item.keyword = aOldKeyword;
this.item.href = (PlacesUtils.bookmarks.getBookmarkURI(aItemId)).spec;
this.new = new TransactionItemCache();
this.new.keyword = aNewKeyword;
this.new.postData = aNewPostData
@ -3441,22 +3471,55 @@ PlacesEditBookmarkKeywordTransaction.prototype = {
doTransaction: function EBKTXN_doTransaction()
{
// Store the current values.
this.item.keyword = PlacesUtils.bookmarks.getKeywordForBookmark(this.item.id);
if (this.item.keyword)
this.item.postData = PlacesUtils.getPostDataForBookmark(this.item.id);
let done = false;
Task.spawn(function* () {
if (this.item.keyword) {
let oldEntry = yield PlacesUtils.keywords.fetch(this.item.keyword);
this.item.postData = oldEntry.postData;
yield PlacesUtils.keywords.remove(this.item.keyword);
}
// Update the keyword.
PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.new.keyword);
if (this.new.keyword && this.new.postData)
PlacesUtils.setPostDataForBookmark(this.item.id, this.new.postData);
if (this.new.keyword) {
yield PlacesUtils.keywords.insert({
url: this.item.href,
keyword: this.new.keyword,
postData: this.new.postData || this.item.postData
});
}
}.bind(this)).catch(Cu.reportError)
.then(() => done = true);
// TODO: Until we can move to PlacesTransactions.jsm, we must spin the
// events loop :(
let thread = Services.tm.currentThread;
while (!done) {
thread.processNextEvent(true);
}
},
undoTransaction: function EBKTXN_undoTransaction()
{
PlacesUtils.bookmarks.setKeywordForBookmark(this.item.id, this.item.keyword);
if (this.item.postData)
PlacesUtils.setPostDataForBookmark(this.item.id, this.item.postData);
let done = false;
Task.spawn(function* () {
if (this.new.keyword) {
yield PlacesUtils.keywords.remove(this.new.keyword);
}
if (this.item.keyword) {
yield PlacesUtils.keywords.insert({
url: this.item.href,
keyword: this.item.keyword,
postData: this.item.postData
});
}
}.bind(this)).catch(Cu.reportError)
.then(() => done = true);
// TODO: Until we can move to PlacesTransactions.jsm, we must spin the
// events loop :(
let thread = Services.tm.currentThread;
while (!done) {
thread.processNextEvent(true);
}
}
};

View File

@ -3,7 +3,7 @@
* 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 CURRENT_SCHEMA_VERSION = 33;
const CURRENT_SCHEMA_VERSION = 34;
const FIRST_UPGRADABLE_SCHEMA_VERSION = 11;
const NS_APP_USER_PROFILE_50_DIR = "ProfD";

View File

@ -20,6 +20,7 @@ support-files =
places_v31.sqlite
places_v32.sqlite
places_v33.sqlite
places_v34.sqlite
[test_current_from_downgraded.js]
[test_current_from_v6.js]

View File

@ -681,11 +681,12 @@ add_task(function* test_remove_folder() {
});
add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
const testURI = NetUtil.newURI("http://add.remove.tag")
, TAG_1 = "TestTag1", TAG_2 = "TestTag2"
, KEYWORD = "test_keyword"
, POST_DATA = "post_data"
, ANNO = { name: "TestAnno", value: "TestAnnoValue" };
const testURI = NetUtil.newURI("http://add.remove.tag");
const TAG_1 = "TestTag1";
const TAG_2 = "TestTag2";
const KEYWORD = "test_keyword";
const POST_DATA = "post_data";
const ANNO = { name: "TestAnno", value: "TestAnnoValue" };
let folder_info = createTestFolderInfo();
folder_info.guid = yield PT.NewFolder(folder_info).transact();
@ -749,9 +750,14 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
ensureTags([TAG_1]);
// Check if Remove correctly restores keywords, tags and annotations.
// Since both bookmarks share the same uri, they also share the keyword that
// is not removed along with one of the bookmarks.
observer.reset();
yield PT.redo();
ensureItemsChanged(...b2_post_creation_changes);
ensureItemsChanged({ guid: b2_info.guid
, isAnnoProperty: true
, property: ANNO.name
, newValue: ANNO.value });
ensureTags([TAG_1, TAG_2]);
// Test Remove for multiple items.
@ -761,6 +767,10 @@ add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
yield PT.Remove(folder_info.guid).transact();
yield ensureItemsRemoved(b1_info, b2_info, folder_info);
ensureTags([]);
// There is no keyword removal notification cause all bookmarks are removed
// before the keyword itself, so there's no one to notify.
let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
Assert.equal(entry, null, "keyword has been removed");
observer.reset();
yield PT.undo();
@ -1021,16 +1031,24 @@ add_task(function* test_edit_keyword() {
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
observer.reset();
yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD }).transact();
yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD, postData: "postData" }).transact();
ensureKeywordChange(KEYWORD);
let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData");
observer.reset();
yield PT.undo();
ensureKeywordChange();
entry = yield PlacesUtils.keywords.fetch(KEYWORD);
Assert.equal(entry, null);
observer.reset();
yield PT.redo();
ensureKeywordChange(KEYWORD);
entry = yield PlacesUtils.keywords.fetch(KEYWORD);
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData");
// Cleanup
observer.reset();
@ -1043,6 +1061,68 @@ add_task(function* test_edit_keyword() {
ensureUndoState();
});
add_task(function* test_edit_specific_keyword() {
let bm_info = { parentGuid: rootGuid
, url: NetUtil.newURI("http://test.edit.keyword") };
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") {
ensureItemsChanged({ guid: bm_info.guid
, property: "keyword"
, newValue: aCurrentKeyword
});
}
yield PlacesUtils.keywords.insert({ keyword: "kw1", url: bm_info.url.spec, postData: "postData1" });
yield PlacesUtils.keywords.insert({ keyword: "kw2", url: bm_info.url.spec, postData: "postData2" });
bm_info.guid = yield PT.NewBookmark(bm_info).transact();
observer.reset();
yield PT.EditKeyword({ guid: bm_info.guid, keyword: "keyword", oldKeyword: "kw2" }).transact();
ensureKeywordChange("keyword", "kw2");
let entry = yield PlacesUtils.keywords.fetch("kw1");
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData1");
entry = yield PlacesUtils.keywords.fetch("keyword");
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData2");
entry = yield PlacesUtils.keywords.fetch("kw2");
Assert.equal(entry, null);
observer.reset();
yield PT.undo();
ensureKeywordChange("kw2", "keyword");
entry = yield PlacesUtils.keywords.fetch("kw1");
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData1");
entry = yield PlacesUtils.keywords.fetch("kw2");
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData2");
entry = yield PlacesUtils.keywords.fetch("keyword");
Assert.equal(entry, null);
observer.reset();
yield PT.redo();
ensureKeywordChange("keyword", "kw2");
entry = yield PlacesUtils.keywords.fetch("kw1");
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData1");
entry = yield PlacesUtils.keywords.fetch("keyword");
Assert.equal(entry.url.href, bm_info.url.spec);
Assert.equal(entry.postData, "postData2");
entry = yield PlacesUtils.keywords.fetch("kw2");
Assert.equal(entry, null);
// Cleanup
observer.reset();
yield PT.undo();
ensureKeywordChange("kw2");
yield PT.undo();
ensureItemsRemoved(bm_info);
yield PT.clearTransactionsHistory();
ensureUndoState();
});
add_task(function* test_tag_uri() {
// This also tests passing uri specs.
let bm_info_a = { url: "http://bookmarked.uri"

View File

@ -525,6 +525,24 @@ add_task(function* test_oldKeywordsAPI() {
check_no_orphans();
});
function run_test() {
run_next_test();
}
add_task(function* test_bookmarkURLChange() {
let fc1 = yield foreign_count("http://example1.com/");
let fc2 = yield foreign_count("http://example2.com/");
let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/",
type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid });
yield PlacesUtils.keywords.insert({ keyword: "keyword",
url: "http://example1.com/" });
yield check_keyword(true, "http://example1.com/", "keyword");
Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword
yield PlacesUtils.bookmarks.update({ guid: bookmark.guid,
url: "http://example2.com/"});
yield promiseKeyword("keyword", "http://example2.com/");
yield check_keyword(false, "http://example1.com/", "keyword");
yield check_keyword(true, "http://example2.com/", "keyword");
Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark -1 keyword
Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 bookmark +1 keyword
});

View File

@ -555,6 +555,40 @@ add_task(function* test_edit_keyword() {
do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), null);
});
add_task(function* test_edit_specific_keyword() {
const KEYWORD = "keyword-test_edit_keyword2";
let testURI = NetUtil.newURI("http://test_edit_keyword2.com");
let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword");
// Add multiple keyword to this uri.
yield PlacesUtils.keywords.insert({ keyword: "kw1", url: testURI.spec, postData: "postData1" });
yield PlacesUtils.keywords.insert({keyword: "kw2", url: testURI.spec, postData: "postData2" });
// Try to change only kw2.
let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData2", "kw2");
txn.doTransaction();
do_check_eq(observer._itemChangedId, testBkmId);
do_check_eq(observer._itemChangedProperty, "keyword");
do_check_eq(observer._itemChangedValue, KEYWORD);
let entry = yield PlacesUtils.keywords.fetch("kw1");
Assert.equal(entry.url.href, testURI.spec);
Assert.equal(entry.postData, "postData1");
yield promiseKeyword(KEYWORD, testURI.spec, "postData2");
yield promiseKeyword("kw2", null);
txn.undoTransaction();
do_check_eq(observer._itemChangedId, testBkmId);
do_check_eq(observer._itemChangedProperty, "keyword");
do_check_eq(observer._itemChangedValue, "kw2");
do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData1");
entry = yield PlacesUtils.keywords.fetch("kw1");
Assert.equal(entry.url.href, testURI.spec);
Assert.equal(entry.postData, "postData1");
yield promiseKeyword("kw2", testURI.spec, "postData2");
yield promiseKeyword("keyword", null);
});
add_task(function* test_LoadInSidebar_transaction() {
let testURI = NetUtil.newURI("http://test_LoadInSidebar_transaction.com");
let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test LoadInSidebar transaction");