"use strict" function* check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) { // Check case-insensitivity. aKeyword = aKeyword.toUpperCase(); let entry = yield PlacesUtils.keywords.fetch(aKeyword); Assert.deepEqual(entry, yield 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, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })); let entries = []; yield 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"); Assert.equal(null, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })); } } /** * Polls the keywords cache waiting for the given keyword entry. */ function* promiseKeyword(keyword, expectedHref) { let href = null; do { yield new Promise(resolve => do_timeout(100, resolve)); let entry = yield PlacesUtils.keywords.fetch(keyword); if (entry) href = entry.url.href; } while (href != expectedHref); } function* check_no_orphans() { let db = yield PlacesUtils.promiseDBConnection(); let rows = yield 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() { let notifications = []; let observer = new Proxy(NavBookmarkObserver, { get(target, name) { if (name == "check") { PlacesUtils.bookmarks.removeObserver(observer); return expectedNotifications => Assert.deepEqual(notifications, expectedNotifications); } if (name.startsWith("onItemChanged")) { return function(itemId, property) { if (property != "keyword") return; let args = Array.from(arguments, arg => { if (arg && arg instanceof Ci.nsIURI) return new URL(arg.spec); if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000) return new Date(parseInt(arg/1000)); return arg; }); notifications.push({ name: name, arguments: args }); } } if (name in target) return target[name]; return undefined; } }); PlacesUtils.bookmarks.addObserver(observer, false); return observer; } add_task(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(function* test_addKeyword() { yield check_keyword(false, "http://example.com/", "keyword"); let fc = yield foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); observer.check([]); yield check_keyword(true, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword // Now remove the keyword. observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.remove("keyword"); observer.check([]); yield check_keyword(false, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword // Check using URL. yield PlacesUtils.keywords.insert({ keyword: "keyword", url: new URL("http://example.com/") }); yield check_keyword(true, "http://example.com/", "keyword"); yield PlacesUtils.keywords.remove("keyword"); yield check_keyword(false, "http://example.com/", "keyword"); yield check_no_orphans(); }); add_task(function* test_addBookmarkAndKeyword() { yield check_keyword(false, "http://example.com/", "keyword"); let fc = yield foreign_count("http://example.com/"); let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); let observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), "keyword", false, "keyword", bookmark.lastModified, bookmark.type, (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), bookmark.guid, bookmark.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); yield check_keyword(true, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword // Now remove the keyword. observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.remove("keyword"); observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), "keyword", false, "", bookmark.lastModified, bookmark.type, (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), bookmark.guid, bookmark.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); yield check_keyword(false, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // -1 keyword // Add again the keyword, then remove the bookmark. yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); observer = expectBookmarkNotifications(); yield PlacesUtils.bookmarks.remove(bookmark.guid); // the notification is synchronous but the removal process is async. // Unfortunately there's nothing explicit we can wait for. while ((yield foreign_count("http://example.com/"))); // We don't get any itemChanged notification since the bookmark has been // removed already. observer.check([]); yield check_keyword(false, "http://example.com/", "keyword"); yield check_no_orphans(); }); add_task(function* test_addKeywordToURIHavingKeyword() { yield check_keyword(false, "http://example.com/", "keyword"); let fc = yield foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); observer.check([]); yield check_keyword(true, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" }); yield check_keyword(true, "http://example.com/", "keyword"); yield check_keyword(true, "http://example.com/", "keyword2"); Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 keyword let entries = []; let entry = yield 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(); yield PlacesUtils.keywords.remove("keyword"); yield PlacesUtils.keywords.remove("keyword2"); observer.check([]); yield check_keyword(false, "http://example.com/", "keyword"); yield check_keyword(false, "http://example.com/", "keyword2"); Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword yield check_no_orphans(); }); add_task(function* test_addBookmarkToURIHavingKeyword() { yield check_keyword(false, "http://example.com/", "keyword"); let fc = yield foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); observer.check([]); yield check_keyword(true, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword observer = expectBookmarkNotifications(); let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark observer.check([]); observer = expectBookmarkNotifications(); yield PlacesUtils.bookmarks.remove(bookmark.guid); // the notification is synchronous but the removal process is async. // Unfortunately there's nothing explicit we can wait for. while ((yield foreign_count("http://example.com/"))); // We don't get any itemChanged notification since the bookmark has been // removed already. observer.check([]); yield check_keyword(false, "http://example.com/", "keyword"); yield check_no_orphans(); }); add_task(function* test_sameKeywordDifferentURL() { let fc1 = yield foreign_count("http://example1.com/"); let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); let fc2 = yield foreign_count("http://example2.com/"); let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example2.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 check_keyword(false, "http://example2.com/", "keyword"); Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // +1 bookmark // Assign the same keyword to another url. let observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example2.com/" }); observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), "keyword", false, "", bookmark1.lastModified, bookmark1.type, (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), bookmark1.guid, bookmark1.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, { name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), "keyword", false, "keyword", bookmark2.lastModified, bookmark2.type, (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), bookmark2.guid, bookmark2.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); yield check_keyword(false, "http://example1.com/", "keyword"); Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); // -1 keyword yield check_keyword(true, "http://example2.com/", "keyword"); Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 keyword // Now remove the keyword. observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.remove("keyword"); observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), "keyword", false, "", bookmark2.lastModified, bookmark2.type, (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), bookmark2.guid, bookmark2.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); yield check_keyword(false, "http://example1.com/", "keyword"); yield check_keyword(false, "http://example2.com/", "keyword"); Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // -1 keyword yield PlacesUtils.bookmarks.remove(bookmark1); yield PlacesUtils.bookmarks.remove(bookmark2); Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark while ((yield foreign_count("http://example2.com/"))); // -1 keyword yield check_no_orphans(); }); add_task(function* test_sameURIDifferentKeyword() { let fc = yield foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); yield PlacesUtils.keywords.insert({keyword: "keyword", url: "http://example.com/" }); yield check_keyword(true, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), "keyword", false, "keyword", bookmark.lastModified, bookmark.type, (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), bookmark.guid, bookmark.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" }); yield check_keyword(true, "http://example.com/", "keyword"); yield check_keyword(true, "http://example.com/", "keyword2"); Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +1 keyword observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), "keyword", false, "keyword2", bookmark.lastModified, bookmark.type, (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), bookmark.guid, bookmark.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); // Add a third keyword. yield PlacesUtils.keywords.insert({ keyword: "keyword3", url: "http://example.com/" }); yield check_keyword(true, "http://example.com/", "keyword"); yield check_keyword(true, "http://example.com/", "keyword2"); yield check_keyword(true, "http://example.com/", "keyword3"); Assert.equal((yield foreign_count("http://example.com/")), fc + 4); // +1 keyword // Remove one of the keywords. observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.remove("keyword"); yield check_keyword(false, "http://example.com/", "keyword"); yield check_keyword(true, "http://example.com/", "keyword2"); yield check_keyword(true, "http://example.com/", "keyword3"); observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)), "keyword", false, "", bookmark.lastModified, bookmark.type, (yield PlacesUtils.promiseItemId(bookmark.parentGuid)), bookmark.guid, bookmark.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // -1 keyword // Now remove the bookmark. yield PlacesUtils.bookmarks.remove(bookmark); while ((yield foreign_count("http://example.com/"))); yield check_keyword(false, "http://example.com/", "keyword"); yield check_keyword(false, "http://example.com/", "keyword2"); yield check_keyword(false, "http://example.com/", "keyword3"); check_no_orphans(); }); add_task(function* test_deleteKeywordMultipleBookmarks() { let fc = yield foreign_count("http://example.com/"); let observer = expectBookmarkNotifications(); let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); yield check_keyword(true, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +2 bookmark +1 keyword observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), "keyword", false, "keyword", bookmark2.lastModified, bookmark2.type, (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), bookmark2.guid, bookmark2.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, { name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), "keyword", false, "keyword", bookmark1.lastModified, bookmark1.type, (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), bookmark1.guid, bookmark1.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); observer = expectBookmarkNotifications(); yield PlacesUtils.keywords.remove("keyword"); yield check_keyword(false, "http://example.com/", "keyword"); Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // -1 keyword observer.check([{ name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)), "keyword", false, "", bookmark2.lastModified, bookmark2.type, (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)), bookmark2.guid, bookmark2.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] }, { name: "onItemChanged", arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)), "keyword", false, "", bookmark1.lastModified, bookmark1.type, (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)), bookmark1.guid, bookmark1.parentGuid, "", Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]); // Now remove the bookmarks. yield PlacesUtils.bookmarks.remove(bookmark1); yield PlacesUtils.bookmarks.remove(bookmark2); Assert.equal((yield foreign_count("http://example.com/")), fc); // -2 bookmarks check_no_orphans(); }); add_task(function* test_multipleKeywordsSamePostData() { yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", postData: "postData1" }); yield check_keyword(true, "http://example.com/", "keyword", "postData1"); // Add another keyword with same postData, should fail. yield Assert.rejects(PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "postData1" }), /constraint failed/); yield check_keyword(false, "http://example.com/", "keyword2", "postData1"); yield PlacesUtils.keywords.remove("keyword"); check_no_orphans(); }); add_task(function* test_oldPostDataAPI() { let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" }); let itemId = yield PlacesUtils.promiseItemId(bookmark.guid); yield PlacesUtils.setPostDataForBookmark(itemId, "postData"); yield check_keyword(true, "http://example.com/", "keyword", "postData"); Assert.equal(PlacesUtils.getPostDataForBookmark(itemId), "postData"); yield PlacesUtils.keywords.remove("keyword"); yield PlacesUtils.bookmarks.remove(bookmark); check_no_orphans(); }); add_task(function* test_oldKeywordsAPI() { let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/", type: PlacesUtils.bookmarks.TYPE_BOOKMARK, parentGuid: PlacesUtils.bookmarks.unfiledGuid }); yield check_keyword(false, "http://example.com/", "keyword"); let itemId = yield PlacesUtils.promiseItemId(bookmark.guid); PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword"); yield promiseKeyword("keyword", "http://example.com/"); // Remove the keyword. PlacesUtils.bookmarks.setKeywordForBookmark(itemId, ""); yield promiseKeyword("keyword", null); yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com" }); Assert.equal(PlacesUtils.bookmarks.getKeywordForBookmark(itemId), "keyword"); Assert.equal(PlacesUtils.bookmarks.getURIForKeyword("keyword").spec, "http://example.com/"); yield PlacesUtils.bookmarks.remove(bookmark); check_no_orphans(); }); 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 });