"use strict";
ChromeUtils.defineESModuleGetters(
this, {
Preferences:
"resource://gre/modules/Preferences.sys.mjs",
});
async
function check_keyword(aExpectExists, aHref, aKeyword, aPostData =
null) {
// Check case-insensitivity.
aKeyword = aKeyword.toUpperCase();
let entry = await PlacesUtils.keywords.fetch(aKeyword);
Assert.deepEqual(
entry,
await PlacesUtils.keywords.fetch({ keyword: aKeyword })
);
if (aExpectExists) {
Assert.ok(!!entry,
"A keyword should exist");
Assert.equal(entry.url.href, aHref);
Assert.equal(entry.postData, aPostData);
Assert.deepEqual(
entry,
await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })
);
let entries = [];
await PlacesUtils.keywords.fetch({ url: aHref }, e => entries.push(e));
Assert.ok(
entries.some(
e => e.url.href == aHref && e.keyword == aKeyword.toLowerCase()
)
);
}
else {
Assert.ok(
!entry || entry.url.href != aHref,
"The given keyword entry should not exist"
);
if (aHref) {
Assert.equal(
null,
await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })
);
}
else {
Assert.equal(
null,
await PlacesUtils.keywords.fetch({ keyword: aKeyword })
);
}
}
}
/**
* Polls the keywords cache waiting for the given keyword entry.
*/
async
function promiseKeyword(keyword, expectedHref) {
let href =
null;
do {
await
new Promise(resolve => do_timeout(100, resolve));
let entry = await PlacesUtils.keywords.fetch(keyword);
if (entry) {
href = entry.url.href;
}
}
while (href != expectedHref);
}
async
function check_no_orphans() {
let db = await PlacesUtils.promiseDBConnection();
let rows = await db.executeCached(
`SELECT id FROM moz_keywords k
WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id)
`
);
Assert.equal(rows.length, 0);
}
function expectBookmarkNotifications() {
const observer = {
notifications: [],
_start() {
this._handle =
this._handle.bind(
this);
PlacesUtils.observers.addListener(
[
"bookmark-keyword-changed"],
this._handle
);
},
_handle(events) {
for (
const event of events) {
this.notifications.push({
type: event.type,
id: event.id,
itemType: event.itemType,
url: event.url,
guid: event.guid,
parentGuid: event.parentGuid,
keyword: event.keyword,
lastModified:
new Date(event.lastModified),
source: event.source,
isTagging: event.isTagging,
});
}
},
check(expected) {
PlacesUtils.observers.removeListener(
[
"bookmark-keyword-changed"],
this._handle
);
Assert.deepEqual(
this.notifications, expected);
},
};
observer._start();
return observer;
}
add_task(async
function test_invalid_input() {
Assert.
throws(() => PlacesUtils.keywords.fetch(
null), /Invalid keyword/);
Assert.
throws(() => PlacesUtils.keywords.fetch(5), /Invalid keyword/);
Assert.
throws(() => PlacesUtils.keywords.fetch(undefined), /Invalid keyword/);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ keyword:
null }),
/Invalid keyword/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ keyword: {} }),
/Invalid keyword/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ keyword: 5 }),
/Invalid keyword/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({}),
/At least keyword or url must be provided/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ keyword:
"test" },
"test"),
/onResult callback must be a valid
function/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ url:
"test" }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ url: {} }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ url:
null }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.fetch({ url:
"" }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.insert(
null),
/Input should be a valid object/
);
Assert.
throws(
() => PlacesUtils.keywords.insert(
"test"),
/Input should be a valid object/
);
Assert.
throws(
() => PlacesUtils.keywords.insert(undefined),
/Input should be a valid object/
);
Assert.
throws(() => PlacesUtils.keywords.insert({}), /Invalid keyword/);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
null }),
/Invalid keyword/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword: 5 }),
/Invalid keyword/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"" }),
/Invalid keyword/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"test", postData: 5 }),
/Invalid POST data/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"test", postData: {} }),
/Invalid POST data/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"test" }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"test", url: 5 }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"test", url:
"" }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"test", url:
null }),
/is not a valid URL/
);
Assert.
throws(
() => PlacesUtils.keywords.insert({ keyword:
"test", url:
"mozilla" }),
/is not a valid URL/
);
Assert.
throws(() => PlacesUtils.keywords.remove(
null), /Invalid keyword/);
Assert.
throws(() => PlacesUtils.keywords.remove(
""), /Invalid keyword/);
Assert.
throws(() => PlacesUtils.keywords.remove(5), /Invalid keyword/);
});
add_task(async
function test_addKeyword() {
await check_keyword(
false,
"http://example.com/", "keyword");
let fc = await foreign_count(
"http://example.com/");
let observer = expectBookmarkNotifications();
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
});
observer.check([]);
await check_keyword(
true,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 1); // +1 keyword
// Now remove the keyword.
observer = expectBookmarkNotifications();
await PlacesUtils.keywords.remove(
"keyword");
observer.check([]);
await check_keyword(
false,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc); // -1 keyword
// Check using URL.
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
new URL(
"http://example.com/"),
});
await check_keyword(
true,
"http://example.com/", "keyword");
await PlacesUtils.keywords.remove(
"keyword");
await check_keyword(
false,
"http://example.com/", "keyword");
await check_no_orphans();
});
add_task(async
function test_addBookmarkAndKeyword() {
let timerPrecision = Preferences.get(
"privacy.reduceTimerPrecision");
Preferences.set(
"privacy.reduceTimerPrecision",
false);
registerCleanupFunction(
function () {
Preferences.set(
"privacy.reduceTimerPrecision", timerPrecision);
});
await check_keyword(
false,
"http://example.com/", "keyword");
let fc = await foreign_count(
"http://example.com/");
let bookmark = await PlacesUtils.bookmarks.insert({
url:
"http://example.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
let observer = expectBookmarkNotifications();
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
});
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark.guid),
itemType: bookmark.type,
url: bookmark.url,
guid: bookmark.guid,
parentGuid: bookmark.parentGuid,
keyword:
"keyword",
lastModified:
new Date(bookmark.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
await check_keyword(
true,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 2); // +1 bookmark +1 keyword
// Now remove the keyword.
observer = expectBookmarkNotifications();
await PlacesUtils.keywords.remove(
"keyword");
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark.guid),
itemType: bookmark.type,
url: bookmark.url,
guid: bookmark.guid,
parentGuid: bookmark.parentGuid,
keyword:
"",
lastModified:
new Date(bookmark.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
await check_keyword(
false,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 1); // -1 keyword
// Add again the keyword, then remove the bookmark.
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
});
observer = expectBookmarkNotifications();
await PlacesUtils.bookmarks.remove(bookmark.guid);
// the notification is synchronous but the removal process is async.
// Unfortunately there's nothing explicit we can wait for.
// eslint-disable-next-line no-empty
while (await foreign_count(
"http://example.com/")) {}
// We don't get any itemChanged notification since the bookmark has been
// removed already.
observer.check([]);
await check_keyword(
false,
"http://example.com/", "keyword");
await check_no_orphans();
});
add_task(async
function test_addKeywordToURIHavingKeyword() {
await check_keyword(
false,
"http://example.com/", "keyword");
let fc = await foreign_count(
"http://example.com/");
let observer = expectBookmarkNotifications();
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
});
observer.check([]);
await check_keyword(
true,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 1); // +1 keyword
await PlacesUtils.keywords.insert({
keyword:
"keyword2",
url:
"http://example.com/",
postData:
"test=1",
});
await check_keyword(
true,
"http://example.com/", "keyword");
await check_keyword(
true,
"http://example.com/", "keyword2", "test=1");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 2); // +1 keyword
let entries = [];
let entry = await PlacesUtils.keywords.fetch(
{ url:
"http://example.com/" },
e => entries.push(e)
);
Assert.equal(entries.length, 2);
Assert.deepEqual(entries[0], entry);
// Now remove the keywords.
observer = expectBookmarkNotifications();
await PlacesUtils.keywords.remove(
"keyword");
await PlacesUtils.keywords.remove(
"keyword2");
observer.check([]);
await check_keyword(
false,
"http://example.com/", "keyword");
await check_keyword(
false,
"http://example.com/", "keyword2");
Assert.equal(await foreign_count(
"http://example.com/"), fc); // -1 keyword
await check_no_orphans();
});
add_task(async
function test_addBookmarkToURIHavingKeyword() {
await check_keyword(
false,
"http://example.com/", "keyword");
let fc = await foreign_count(
"http://example.com/");
let observer = expectBookmarkNotifications();
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
});
observer.check([]);
await check_keyword(
true,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 1); // +1 keyword
observer = expectBookmarkNotifications();
let bookmark = await PlacesUtils.bookmarks.insert({
url:
"http://example.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
Assert.equal(await foreign_count(
"http://example.com/"), fc + 2); // +1 bookmark
observer.check([]);
observer = expectBookmarkNotifications();
await PlacesUtils.bookmarks.remove(bookmark.guid);
// the notification is synchronous but the removal process is async.
// Unfortunately there's nothing explicit we can wait for.
// eslint-disable-next-line no-empty
while (await foreign_count(
"http://example.com/")) {}
// We don't get any itemChanged notification since the bookmark has been
// removed already.
observer.check([]);
await check_keyword(
false,
"http://example.com/", "keyword");
await check_no_orphans();
});
add_task(async
function test_sameKeywordDifferentURL() {
let fc1 = await foreign_count(
"http://example1.com/");
let bookmark1 = await PlacesUtils.bookmarks.insert({
url:
"http://example1.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
let fc2 = await foreign_count(
"http://example2.com/");
let bookmark2 = await PlacesUtils.bookmarks.insert({
url:
"http://example2.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example1.com/",
});
await check_keyword(
true,
"http://example1.com/", "keyword");
Assert.equal(await foreign_count(
"http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword
await check_keyword(
false,
"http://example2.com/", "keyword");
Assert.equal(await foreign_count(
"http://example2.com/"), fc2 + 1); // +1 bookmark
// Assign the same keyword to another url.
let observer = expectBookmarkNotifications();
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example2.com/",
});
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark1.guid),
itemType: bookmark1.type,
url: bookmark1.url,
guid: bookmark1.guid,
parentGuid: bookmark1.parentGuid,
keyword:
"",
lastModified:
new Date(bookmark1.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
itemType: bookmark2.type,
url: bookmark2.url,
guid: bookmark2.guid,
parentGuid: bookmark2.parentGuid,
keyword:
"keyword",
lastModified:
new Date(bookmark2.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
await check_keyword(
false,
"http://example1.com/", "keyword");
Assert.equal(await foreign_count(
"http://example1.com/"), fc1 + 1); // -1 keyword
await check_keyword(
true,
"http://example2.com/", "keyword");
Assert.equal(await foreign_count(
"http://example2.com/"), fc2 + 2); // +1 keyword
// Now remove the keyword.
observer = expectBookmarkNotifications();
await PlacesUtils.keywords.remove(
"keyword");
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
itemType: bookmark2.type,
url: bookmark2.url,
guid: bookmark2.guid,
parentGuid: bookmark2.parentGuid,
keyword:
"",
lastModified:
new Date(bookmark2.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
await check_keyword(
false,
"http://example1.com/", "keyword");
await check_keyword(
false,
"http://example2.com/", "keyword");
Assert.equal(await foreign_count(
"http://example1.com/"), fc1 + 1);
Assert.equal(await foreign_count(
"http://example2.com/"), fc2 + 1); // -1 keyword
await PlacesUtils.bookmarks.remove(bookmark1);
await PlacesUtils.bookmarks.remove(bookmark2);
Assert.equal(await foreign_count(
"http://example1.com/"), fc1); // -1 bookmark
// eslint-disable-next-line no-empty
while (await foreign_count(
"http://example2.com/")) {} // -1 keyword
await check_no_orphans();
});
add_task(async
function test_sameURIDifferentKeyword() {
let fc = await foreign_count(
"http://example.com/");
let observer = expectBookmarkNotifications();
let bookmark = await PlacesUtils.bookmarks.insert({
url:
"http://example.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
});
await check_keyword(
true,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 2); // +1 bookmark +1 keyword
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark.guid),
itemType: bookmark.type,
url: bookmark.url,
guid: bookmark.guid,
parentGuid: bookmark.parentGuid,
keyword:
"keyword",
lastModified:
new Date(bookmark.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
observer = expectBookmarkNotifications();
await PlacesUtils.keywords.insert({
keyword:
"keyword2",
url:
"http://example.com/",
});
await check_keyword(
false,
"http://example.com/", "keyword");
await check_keyword(
true,
"http://example.com/", "keyword2");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 2); // -1 keyword +1 keyword
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark.guid),
itemType: bookmark.type,
url: bookmark.url,
guid: bookmark.guid,
parentGuid: bookmark.parentGuid,
keyword:
"keyword2",
lastModified:
new Date(bookmark.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
// Now remove the bookmark.
await PlacesUtils.bookmarks.remove(bookmark);
// eslint-disable-next-line no-empty
while (await foreign_count(
"http://example.com/")) {}
await check_keyword(
false,
"http://example.com/", "keyword");
await check_keyword(
false,
"http://example.com/", "keyword2");
await check_no_orphans();
});
add_task(async
function test_deleteKeywordMultipleBookmarks() {
let fc = await foreign_count(
"http://example.com/");
let observer = expectBookmarkNotifications();
let bookmark1 = await PlacesUtils.bookmarks.insert({
url:
"http://example.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
let bookmark2 = await PlacesUtils.bookmarks.insert({
url:
"http://example.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
});
await check_keyword(
true,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 3); // +2 bookmark +1 keyword
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
itemType: bookmark2.type,
url: bookmark2.url,
guid: bookmark2.guid,
parentGuid: bookmark2.parentGuid,
keyword:
"keyword",
lastModified:
new Date(bookmark2.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark1.guid),
itemType: bookmark1.type,
url: bookmark1.url,
guid: bookmark1.guid,
parentGuid: bookmark1.parentGuid,
keyword:
"keyword",
lastModified:
new Date(bookmark1.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
observer = expectBookmarkNotifications();
await PlacesUtils.keywords.remove(
"keyword");
await check_keyword(
false,
"http://example.com/", "keyword");
Assert.equal(await foreign_count(
"http://example.com/"), fc + 2); // -1 keyword
observer.check([
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
itemType: bookmark2.type,
url: bookmark2.url,
guid: bookmark2.guid,
parentGuid: bookmark2.parentGuid,
keyword:
"",
lastModified:
new Date(bookmark2.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
{
type:
"bookmark-keyword-changed",
id: await PlacesTestUtils.promiseItemId(bookmark1.guid),
itemType: bookmark1.type,
url: bookmark1.url,
guid: bookmark1.guid,
parentGuid: bookmark1.parentGuid,
keyword:
"",
lastModified:
new Date(bookmark1.lastModified),
source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
isTagging:
false,
},
]);
// Now remove the bookmarks.
await PlacesUtils.bookmarks.remove(bookmark1);
await PlacesUtils.bookmarks.remove(bookmark2);
Assert.equal(await foreign_count(
"http://example.com/"), fc); // -2 bookmarks
await check_no_orphans();
});
add_task(async
function test_multipleKeywordsSamePostData() {
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example.com/",
postData:
"postData1",
});
await check_keyword(
true,
"http://example.com/", "keyword", "postData1");
// Add another keyword with same postData, should fail.
await PlacesUtils.keywords.insert({
keyword:
"keyword2",
url:
"http://example.com/",
postData:
"postData1",
});
await check_keyword(
false,
"http://example.com/", "keyword", "postData1");
await check_keyword(
true,
"http://example.com/", "keyword2", "postData1");
await PlacesUtils.keywords.remove(
"keyword2");
await check_no_orphans();
});
add_task(async
function test_bookmarkURLChange() {
let fc1 = await foreign_count(
"http://example1.com/");
let fc2 = await foreign_count(
"http://example2.com/");
let bookmark = await PlacesUtils.bookmarks.insert({
url:
"http://example1.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
await PlacesUtils.keywords.insert({
keyword:
"keyword",
url:
"http://example1.com/",
});
await check_keyword(
true,
"http://example1.com/", "keyword");
Assert.equal(await foreign_count(
"http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword
await PlacesUtils.bookmarks.update({
guid: bookmark.guid,
url:
"http://example2.com/",
});
await promiseKeyword(
"keyword",
"http://example2.com/");
await check_keyword(
false,
"http://example1.com/", "keyword");
await check_keyword(
true,
"http://example2.com/", "keyword");
Assert.equal(await foreign_count(
"http://example1.com/"), fc1); // -1 bookmark -1 keyword
Assert.equal(await foreign_count(
"http://example2.com/"), fc2 + 2); // +1 bookmark +1 keyword
});
add_task(async
function test_tagDoesntPreventKeywordRemoval() {
await check_keyword(
false,
"http://example.com/", "example");
let fc = await foreign_count(
"http://example.com/");
let httpBookmark = await PlacesUtils.bookmarks.insert({
url:
"http://example.com/",
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
});
Assert.equal(await foreign_count(
"http://example.com/"), fc + 1); // +1 bookmark
PlacesUtils.tagging.tagURI(uri(
"http://example.com/"), ["example_tag"]);
Assert.equal(await foreign_count(
"http://example.com/"), fc + 2); // +1 bookmark +1 tag
await PlacesUtils.keywords.insert({
keyword:
"example",
url:
"http://example.com/",
});
Assert.equal(await foreign_count(
"http://example.com/"), fc + 3); // +1 bookmark +1 tag +1 keyword
await check_keyword(
true,
"http://example.com/", "example");
await PlacesUtils.bookmarks.remove(httpBookmark);
await TestUtils.waitForCondition(
async () =>
!(await PlacesUtils.bookmarks.fetch({ url:
"http://example.com/" })),
"Wait for bookmark to be removed"
);
await check_keyword(
false,
"http://example.com/", "example");
Assert.equal(await foreign_count(
"http://example.com/"), fc); // bookmark, keyword, and tag should all have been removed
await check_no_orphans();
});