Bug 1083469 - Allow to use old keywords APIs along with Bookmarks.jsm r=mano

--HG--
extra : rebase_source : 606724d24ed36d9de8045f526d1bb452bf5d0530
This commit is contained in:
Marco Bonardo 2014-11-24 00:51:32 +01:00
parent 3ee27b393c
commit 50013d89bb
7 changed files with 364 additions and 190 deletions

View File

@ -834,6 +834,17 @@ this.PlacesUtils = {
return null;
},
/**
* Gets the href and the post data for a given keyword, if any.
*
* @param keyword
* The string keyword to look for.
* @return {Promise}
* @resolves to a { href, postData } object. Both properties evaluate to null
* if no keyword is found.
*/
promiseHrefAndPostDataForKeyword(keyword) KeywordsCache.promiseEntry(keyword),
/**
* Get the URI (and any associated POST data) for a given keyword.
* @param aKeyword string keyword
@ -1965,6 +1976,114 @@ let GuidHelper = {
}
};
// Cache of bookmarks keywords, used to quickly resolve keyword => URL requests.
let KeywordsCache = {
/**
* Initializes the cache.
* Every method should check _initialized and, if false, yield _initialize().
*/
_initialized: false,
_initialize: Task.async(function* () {
// First populate the cache...
yield this._reloadCache();
// ...then observe changes to keep the cache up-to-date.
PlacesUtils.bookmarks.addObserver(this, false);
PlacesUtils.registerShutdownFunction(() => {
PlacesUtils.bookmarks.removeObserver(this);
});
this._initialized = true;
}),
// nsINavBookmarkObserver
// Manually updating the cache would be tricky because some notifications
// don't report the original bookmark url and we also keep urls sorted by
// last modified. Since changing a keyword-ed bookmark is a rare event,
// it's easier to reload the cache.
onItemChanged(itemId, property, isAnno, val, lastModified, type,
parentId, guid, parentGuid) {
if (property == "keyword" || property == this.POST_DATA_ANNO ||
this._keywordedGuids.has(guid)) {
// Since this cache is used in hot paths, it should be readily available
// as fast as possible.
this._reloadCache().catch(Cu.reportError);
}
},
onItemRemoved(itemId, parentId, index, type, uri, guid, parentGuid) {
if (this._keywordedGuids.has(guid)) {
// Since this cache is used in hot paths, it should be readily available
// as fast as possible.
this._reloadCache().catch(Cu.reportError);
}
},
QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]),
__noSuchMethod__() {}, // Catch all remaining onItem* methods.
// Maps an { href, postData } object to each keyword.
// Even if a keyword may be associated to multiple URLs, only the last
// modified bookmark href is retained here.
_urlDataForKeyword: null,
// Tracks GUIDs having a keyword.
_keywordedGuids: null,
/**
* Reloads the cache.
*/
_reloadPromise: null,
_reloadCache() {
return this._reloadPromise = Task.spawn(function* () {
let db = yield PlacesUtils.promiseDBConnection();
let rows = yield db.execute(
`/* do not warn (bug no) - there is no index on keyword_id */
SELECT b.id, b.guid, h.url, k.keyword FROM moz_bookmarks b
JOIN moz_places h ON h.id = b.fk
JOIN moz_keywords k ON k.id = b.keyword_id
ORDER BY b.lastModified DESC
`);
this._urlDataForKeyword = new Map();
this._keywordedGuids = new Set();
for (let row of rows) {
let guid = row.getResultByName("guid");
this._keywordedGuids.add(guid);
let keyword = row.getResultByName("keyword");
// Only keep the most recent href.
let urlData = this._urlDataForKeyword.get(keyword);
if (urlData)
continue;
let id = row.getResultByName("id");
let href = row.getResultByName("url");
let postData = PlacesUtils.getPostDataForBookmark(id);
this._urlDataForKeyword.set(keyword, { href, postData });
}
}.bind(this)).then(() => {
this._reloadPromise = null;
});
},
/**
* Fetches a { href, postData } entry for the given keyword.
*
* @param keyword
* The keyword to look for.
* @return {promise}
* @resolves when the fetching is complete.
*/
promiseEntry: Task.async(function* (keyword) {
// We could yield regardless and do the checks internally, but that would
// waste at least a couple ticks and this can be used on hot paths.
if (!this._initialized)
yield this._initialize();
if (this._reloadPromise)
yield this._reloadPromise;
return this._urlDataForKeyword.get(keyword) || { href: null, postData: null };
}),
};
////////////////////////////////////////////////////////////////////////////////
//// Transactions handlers.

View File

@ -768,7 +768,7 @@ Search.prototype = {
let hasFirstResult = false;
if (this._searchTokens.length > 0 &&
PlacesUtils.bookmarks.getURIForKeyword(this._searchTokens[0])) {
(yield PlacesUtils.promiseHrefAndPostDataForKeyword(this._searchTokens[0])).href) {
// This may be a keyword of a bookmark.
queries.unshift(this._keywordQuery);
hasFirstResult = true;

View File

@ -19,8 +19,6 @@
#include "GeckoProfiler.h"
#define BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH 32
using namespace mozilla;
// These columns sit to the right of the kGetInfoIndex_* columns.
@ -40,25 +38,6 @@ PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService)
namespace {
struct keywordSearchData
{
int64_t itemId;
nsString keyword;
};
PLDHashOperator
SearchBookmarkForKeyword(nsTrimInt64HashKey::KeyType aKey,
const nsString aValue,
void* aUserArg)
{
keywordSearchData* data = reinterpret_cast<keywordSearchData*>(aUserArg);
if (data->keyword.Equals(aValue)) {
data->itemId = aKey;
return PL_DHASH_STOP;
}
return PL_DHASH_NEXT;
}
template<typename Method, typename DataType>
class AsyncGetBookmarksForURI : public AsyncStatementCallback
{
@ -143,8 +122,6 @@ nsNavBookmarks::nsNavBookmarks()
, mCanNotify(false)
, mCacheObservers("bookmark-observers")
, mBatching(false)
, mBookmarkToKeywordHash(BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH)
, mBookmarkToKeywordHashInitialized(false)
{
NS_ASSERTION(!gBookmarksService,
"Attempting to create two instances of the service!");
@ -646,7 +623,7 @@ nsNavBookmarks::RemoveItem(int64_t aItemId)
NS_ENSURE_SUCCESS(rv, rv);
}
rv = UpdateKeywordsHashForRemovedBookmark(aItemId);
rv = removeOrphanKeywords();
NS_ENSURE_SUCCESS(rv, rv);
// A broken url should not interrupt the removal process.
@ -1119,7 +1096,7 @@ nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId)
NS_ENSURE_SUCCESS(rv, rv);
}
rv = UpdateKeywordsHashForRemovedBookmark(child.id);
rv = removeOrphanKeywords();
NS_ENSURE_SUCCESS(rv, rv);
}
}
@ -2255,39 +2232,23 @@ nsNavBookmarks::SetItemIndex(int64_t aItemId, int32_t aNewIndex)
nsresult
nsNavBookmarks::UpdateKeywordsHashForRemovedBookmark(int64_t aItemId)
nsNavBookmarks::removeOrphanKeywords()
{
nsAutoString keyword;
if (NS_SUCCEEDED(GetKeywordForBookmark(aItemId, keyword)) &&
!keyword.IsEmpty()) {
nsresult rv = EnsureKeywordsHash();
NS_ENSURE_SUCCESS(rv, rv);
mBookmarkToKeywordHash.Remove(aItemId);
// If the keyword is unused, remove it from the database.
nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
"DELETE FROM moz_keywords "
"WHERE NOT EXISTS ( "
"SELECT id "
"FROM moz_bookmarks "
"WHERE keyword_id = moz_keywords.id "
")"
);
NS_ENSURE_STATE(stmt);
// If the keyword is unused, remove it from the database.
keywordSearchData searchData;
searchData.keyword.Assign(keyword);
searchData.itemId = -1;
mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData);
if (searchData.itemId == -1) {
nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
"DELETE FROM moz_keywords "
"WHERE keyword = :keyword "
"AND NOT EXISTS ( "
"SELECT id "
"FROM moz_bookmarks "
"WHERE keyword_id = moz_keywords.id "
")"
);
NS_ENSURE_STATE(stmt);
nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
nsresult rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt));
NS_ENSURE_SUCCESS(rv, rv);
rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
NS_ENSURE_SUCCESS(rv, rv);
nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt));
NS_ENSURE_SUCCESS(rv, rv);
}
}
return NS_OK;
}
@ -2303,9 +2264,6 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
NS_ENSURE_SUCCESS(rv, rv);
rv = EnsureKeywordsHash();
NS_ENSURE_SUCCESS(rv, rv);
// Shortcuts are always lowercased internally.
nsAutoString keyword(aUserCasedKeyword);
ToLowerCase(keyword);
@ -2331,8 +2289,6 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
mozStorageStatementScoper updateBookmarkScoper(updateBookmarkStmt);
if (keyword.IsEmpty()) {
// Remove keyword association from the hash.
mBookmarkToKeywordHash.Remove(bookmark.id);
rv = updateBookmarkStmt->BindNullByName(NS_LITERAL_CSTRING("keyword"));
}
else {
@ -2350,10 +2306,6 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
rv = newKeywordStmt->Execute();
NS_ENSURE_SUCCESS(rv, rv);
// Add new keyword association to the hash, removing the old one if needed.
if (!oldKeyword.IsEmpty())
mBookmarkToKeywordHash.Remove(bookmark.id);
mBookmarkToKeywordHash.Put(bookmark.id, keyword);
rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
}
NS_ENSURE_SUCCESS(rv, rv);
@ -2411,12 +2363,12 @@ nsNavBookmarks::GetKeywordForURI(nsIURI* aURI, nsAString& aKeyword)
rv = stmt->ExecuteStep(&hasMore);
if (NS_FAILED(rv) || !hasMore) {
aKeyword.SetIsVoid(true);
return NS_OK; // not found: return void keyword string
return NS_OK;
}
// found, get the keyword
rv = stmt->GetString(0, aKeyword);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
@ -2427,16 +2379,28 @@ nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword)
NS_ENSURE_ARG_MIN(aBookmarkId, 1);
aKeyword.Truncate(0);
nsresult rv = EnsureKeywordsHash();
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"/* do not warn (bug no) - there is no index on keyword_id) */ "
"SELECT k.keyword "
"FROM moz_bookmarks b "
"JOIN moz_keywords k ON k.id = b.keyword_id "
"WHERE b.id = :id "
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aBookmarkId);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoString keyword;
if (!mBookmarkToKeywordHash.Get(aBookmarkId, &keyword)) {
bool hasMore = false;
rv = stmt->ExecuteStep(&hasMore);
if (NS_FAILED(rv) || !hasMore) {
aKeyword.SetIsVoid(true);
return NS_OK;
}
else {
aKeyword.Assign(keyword);
}
rv = stmt->GetString(0, aKeyword);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
@ -2454,53 +2418,33 @@ nsNavBookmarks::GetURIForKeyword(const nsAString& aUserCasedKeyword,
nsAutoString keyword(aUserCasedKeyword);
ToLowerCase(keyword);
nsresult rv = EnsureKeywordsHash();
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
"/* do not warn (bug no) - there is no index on keyword_id) */ "
"SELECT url FROM moz_keywords k "
"JOIN moz_bookmarks b ON b.keyword_id = k.id "
"JOIN moz_places h ON b.fk = h.id "
"WHERE k.keyword = :keyword "
"ORDER BY b.dateAdded DESC"
);
NS_ENSURE_STATE(stmt);
mozStorageStatementScoper scoper(stmt);
nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
NS_ENSURE_SUCCESS(rv, rv);
keywordSearchData searchData;
searchData.keyword.Assign(keyword);
searchData.itemId = -1;
mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData);
if (searchData.itemId == -1) {
// Not found.
bool hasMore = false;
rv = stmt->ExecuteStep(&hasMore);
if (NS_FAILED(rv) || !hasMore) {
return NS_OK;
}
rv = GetBookmarkURI(searchData.itemId, aURI);
nsCString url;
rv = stmt->GetUTF8String(0, url);
NS_ENSURE_SUCCESS(rv, rv);
return NS_OK;
}
nsresult
nsNavBookmarks::EnsureKeywordsHash() {
if (mBookmarkToKeywordHashInitialized) {
return NS_OK;
}
mBookmarkToKeywordHashInitialized = true;
nsCOMPtr<mozIStorageStatement> stmt;
nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING(
"SELECT b.id, k.keyword "
"FROM moz_bookmarks b "
"JOIN moz_keywords k ON k.id = b.keyword_id "
), getter_AddRefs(stmt));
rv = NS_NewURI(aURI, url);
NS_ENSURE_SUCCESS(rv, rv);
bool hasMore;
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
int64_t itemId;
rv = stmt->GetInt64(0, &itemId);
NS_ENSURE_SUCCESS(rv, rv);
nsAutoString keyword;
rv = stmt->GetString(1, keyword);
NS_ENSURE_SUCCESS(rv, rv);
mBookmarkToKeywordHash.Put(itemId, keyword);
}
return NS_OK;
}

View File

@ -422,20 +422,9 @@ private:
bool mBatching;
/**
* Always call EnsureKeywordsHash() and check it for errors before actually
* using the hash. Internal keyword methods are already doing that.
* Removes orphan keywords.
*/
nsresult EnsureKeywordsHash();
nsDataHashtable<nsTrimInt64HashKey, nsString> mBookmarkToKeywordHash;
bool mBookmarkToKeywordHashInitialized;
/**
* This function must be called every time a bookmark is removed.
*
* @param aURI
* Uri to test.
*/
nsresult UpdateKeywordsHashForRemovedBookmark(int64_t aItemId);
nsresult removeOrphanKeywords();
};
#endif // nsNavBookmarks_h_

View File

@ -12,6 +12,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
"resource://gre/modules/TelemetryStopwatch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
////////////////////////////////////////////////////////////////////////////////
//// Constants
@ -1451,74 +1453,76 @@ urlInlineComplete.prototype = {
this._listener = aListener;
// Don't autoFill if the search term is recognized as a keyword, otherwise
// it will override default keywords behavior. Note that keywords are
// hashed on first use, so while the first query may delay a little bit,
// next ones will just hit the memory hash.
if (this._currentSearchString.length == 0 || !this._db ||
PlacesUtils.bookmarks.getURIForKeyword(this._currentSearchString)) {
this._finishSearch();
return;
}
// Don't try to autofill if the search term includes any whitespace.
// This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
// tokenizer ends up trimming the search string and returning a value
// that doesn't match it, or is even shorter.
if (/\s/.test(this._currentSearchString)) {
this._finishSearch();
return;
}
// Hosts have no "/" in them.
let lastSlashIndex = this._currentSearchString.lastIndexOf("/");
// Search only URLs if there's a slash in the search string...
if (lastSlashIndex != -1) {
// ...but not if it's exactly at the end of the search string.
if (lastSlashIndex < this._currentSearchString.length - 1)
this._queryURL();
else
Task.spawn(function* () {
// Don't autoFill if the search term is recognized as a keyword, otherwise
// it will override default keywords behavior. Note that keywords are
// hashed on first use, so while the first query may delay a little bit,
// next ones will just hit the memory hash.
if (this._currentSearchString.length == 0 || !this._db ||
(yield PlacesUtils.promiseHrefAndPostDataForKeyword(this._currentSearchString)).href) {
this._finishSearch();
return;
}
// Do a synchronous search on the table of hosts.
let query = this._hostQuery;
query.params.search_string = this._currentSearchString.toLowerCase();
// This is just to measure the delay to reach the UI, not the query time.
TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
let ac = this;
let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
handleResult: function (aResultSet) {
let row = aResultSet.getNextRow();
let trimmedHost = row.getResultByIndex(0);
let untrimmedHost = row.getResultByIndex(1);
// If the untrimmed value doesn't preserve the user's input just
// ignore it and complete to the found host.
if (untrimmedHost &&
!untrimmedHost.toLowerCase().contains(ac._originalSearchString.toLowerCase())) {
untrimmedHost = null;
}
ac._result.appendMatch(ac._strippedPrefix + trimmedHost, "", "", "", untrimmedHost);
// handleCompletion() will cause the result listener to be called, and
// will display the result in the UI.
},
handleError: function (aError) {
Components.utils.reportError(
"URL Inline Complete: An async statement encountered an " +
"error: " + aError.result + ", '" + aError.message + "'");
},
handleCompletion: function (aReason) {
TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
ac._finishSearch();
return;
}
}, this._db);
this._pendingQuery = wrapper.executeAsync([query]);
// Don't try to autofill if the search term includes any whitespace.
// This may confuse completeDefaultIndex cause the AUTOCOMPLETE_MATCH
// tokenizer ends up trimming the search string and returning a value
// that doesn't match it, or is even shorter.
if (/\s/.test(this._currentSearchString)) {
this._finishSearch();
return;
}
// Hosts have no "/" in them.
let lastSlashIndex = this._currentSearchString.lastIndexOf("/");
// Search only URLs if there's a slash in the search string...
if (lastSlashIndex != -1) {
// ...but not if it's exactly at the end of the search string.
if (lastSlashIndex < this._currentSearchString.length - 1)
this._queryURL();
else
this._finishSearch();
return;
}
// Do a synchronous search on the table of hosts.
let query = this._hostQuery;
query.params.search_string = this._currentSearchString.toLowerCase();
// This is just to measure the delay to reach the UI, not the query time.
TelemetryStopwatch.start(DOMAIN_QUERY_TELEMETRY);
let ac = this;
let wrapper = new AutoCompleteStatementCallbackWrapper(this, {
handleResult: function (aResultSet) {
let row = aResultSet.getNextRow();
let trimmedHost = row.getResultByIndex(0);
let untrimmedHost = row.getResultByIndex(1);
// If the untrimmed value doesn't preserve the user's input just
// ignore it and complete to the found host.
if (untrimmedHost &&
!untrimmedHost.toLowerCase().contains(ac._originalSearchString.toLowerCase())) {
untrimmedHost = null;
}
ac._result.appendMatch(ac._strippedPrefix + trimmedHost, "", "", "", untrimmedHost);
// handleCompletion() will cause the result listener to be called, and
// will display the result in the UI.
},
handleError: function (aError) {
Components.utils.reportError(
"URL Inline Complete: An async statement encountered an " +
"error: " + aError.result + ", '" + aError.message + "'");
},
handleCompletion: function (aReason) {
TelemetryStopwatch.finish(DOMAIN_QUERY_TELEMETRY);
ac._finishSearch();
}
}, this._db);
this._pendingQuery = wrapper.executeAsync([query]);
}.bind(this));
},
/**

View File

@ -0,0 +1,117 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
add_task(function* test_no_keyword() {
Assert.deepEqual({ href: null, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should not exist");
});
add_task(function* test_add_remove() {
let item1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "http://example1.com/",
keyword: "test" });
Assert.deepEqual({ href: item1.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + item1.url.href);
// Add a second url for the same keyword, since it's newer it should be
// returned.
let item2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "http://example2.com/",
keyword: "test" });
Assert.deepEqual({ href: item2.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + item2.url.href);
// Now remove item2, should return item1 again.
yield PlacesUtils.bookmarks.remove(item2);
Assert.deepEqual({ href: item1.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + item1.url.href);
// Now remove item1, should return null again.
yield PlacesUtils.bookmarks.remove(item1);
Assert.deepEqual({ href: null, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should not exist");
});
add_task(function* test_change_url() {
let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "http://example.com/",
keyword: "test" });
Assert.deepEqual({ href: item.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + item.url.href);
// Change the bookmark url.
let updatedItem = yield PlacesUtils.bookmarks.update({ guid: item.guid,
url: "http://example2.com" });
Assert.deepEqual({ href: updatedItem.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + updatedItem.url.href);
yield PlacesUtils.bookmarks.remove(updatedItem);
});
add_task(function* test_change_keyword() {
let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "http://example.com/",
keyword: "test" });
Assert.deepEqual({ href: item.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + item.url.href);
// Change the bookmark keywprd.
let updatedItem = yield PlacesUtils.bookmarks.update({ guid: item.guid,
keyword: "test2" });
Assert.deepEqual({ href: null, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should not exist");
Assert.deepEqual({ href: updatedItem.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test2")),
"Keyword 'test' should point to " + updatedItem.url.href);
// Remove the bookmark keyword.
updatedItem = yield PlacesUtils.bookmarks.update({ guid: item.guid,
keyword: "" });
Assert.deepEqual({ href: null, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should not exist");
Assert.deepEqual({ href: null, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test2")),
"Keyword 'test' should not exist");
yield PlacesUtils.bookmarks.remove(updatedItem);
});
add_task(function* test_postData() {
let item1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "http://example1.com/",
keyword: "test" });
let itemId1 = yield PlacesUtils.promiseItemId(item1.guid);
PlacesUtils.setPostDataForBookmark(itemId1, "testData");
Assert.deepEqual({ href: item1.url.href, postData: "testData" },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + item1.url.href);
// Add a second url for the same keyword, but without postData.
let item2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
url: "http://example2.com/",
keyword: "test" });
Assert.deepEqual({ href: item2.url.href, postData: null },
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
"Keyword 'test' should point to " + item2.url.href);
yield PlacesUtils.bookmarks.remove(item1);
yield PlacesUtils.bookmarks.remove(item2);
});
function run_test() {
run_next_test();
}

View File

@ -119,6 +119,7 @@ skip-if = true
[test_PlacesSearchAutocompleteProvider.js]
[test_PlacesUtils_asyncGetBookmarkIds.js]
[test_PlacesUtils_lazyobservers.js]
[test_PlacesUtils_promiseHrefAndPostDataForKeyword.js]
[test_placesTxn.js]
[test_preventive_maintenance.js]
# Bug 676989: test hangs consistently on Android