diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /toolkit/components/places/tests/bookmarks | |
parent | Initial commit. (diff) | |
download | firefox-upstream.tar.xz firefox-upstream.zip |
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/places/tests/bookmarks')
44 files changed, 10614 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/bookmarks/bookmarks_long_tag.json b/toolkit/components/places/tests/bookmarks/bookmarks_long_tag.json new file mode 100644 index 0000000000..61e3c2d1ff --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/bookmarks_long_tag.json @@ -0,0 +1,53 @@ +{ + "guid": "root________", + "index": 0, + "id": 1, + "type": "text/x-moz-place-container", + "dateAdded": 1554906792778, + "lastModified": 1554906792778, + "root": "placesRoot", + "children": [{ + "guid": "unfiled_____", + "index": 0, + "id": 2, + "type": "text/x-moz-place-container", + "dateAdded": 1554906792778, + "lastModified": 1554906792778, + "root": "unfiledBookmarksFolder", + "children": [ + { + "guid": "___guid1____", + "index": 0, + "id": 3, + "charset": "UTF-16", + "tags": "tag0", + "type": "text/x-moz-place", + "dateAdded": 1554906792778, + "lastModified": 1554906792778, + "uri": "http://test0.com/" + }, + { + "guid": "___guid2____", + "index": 1, + "id": 4, + "charset": "UTF-16", + "tags": "tag1,a0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789", + "type": "text/x-moz-place", + "dateAdded": 1554906792778, + "lastModified": 1554906792778, + "uri": "http://test1.com/" + }, + { + "guid": "___guid3____", + "index": 2, + "id": 5, + "charset": "UTF-16", + "tags": "tag2", + "type": "text/x-moz-place", + "dateAdded": 1554906792778, + "lastModified": 1554906792778, + "uri": "http://test2.com/" + } + ] + }] +} diff --git a/toolkit/components/places/tests/bookmarks/head_bookmarks.js b/toolkit/components/places/tests/bookmarks/head_bookmarks.js new file mode 100644 index 0000000000..5cb91694bc --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/head_bookmarks.js @@ -0,0 +1,189 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* 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/. */ + +// Import common head. +{ + /* import-globals-from ../head_common.js */ + let commonFile = do_get_file("../head_common.js", false); + let uri = Services.io.newFileURI(commonFile); + Services.scriptloader.loadSubScript(uri.spec, this); +} + +// Put any other stuff relative to this test folder below. + +function expectNotifications(checkAllArgs) { + 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("onItem")) { + return (...origArgs) => { + let args = Array.from(origArgs, arg => { + if (arg && arg instanceof Ci.nsIURI) { + return new URL(arg.spec); + } + if (arg && typeof arg == "number" && arg >= Date.now() * 1000) { + return PlacesUtils.toDate(arg); + } + return arg; + }); + if (checkAllArgs) { + notifications.push({ name, arguments: args }); + } else { + notifications.push({ name, arguments: { guid: args[5] } }); + } + }; + } + + if (name in target) { + return target[name]; + } + return undefined; + }, + }); + PlacesUtils.bookmarks.addObserver(observer); + return observer; +} + +function expectPlacesObserverNotifications( + types, + checkAllArgs = true, + skipDescendants = false +) { + let notifications = []; + let listener = events => { + for (let event of events) { + switch (event.type) { + case "bookmark-added": + notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + parentId: event.parentId, + index: event.index, + url: event.url || undefined, + title: event.title, + dateAdded: new Date(event.dateAdded), + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + isTagging: event.isTagging, + }); + break; + case "bookmark-removed": + if ( + !( + skipDescendants && + event.isDescendantRemoval && + !PlacesUtils.bookmarks.userContentRoots.includes(event.parentGuid) + ) + ) { + if (checkAllArgs) { + notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + parentId: event.parentId, + index: event.index, + url: event.url || null, + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + isTagging: event.isTagging, + }); + } else { + notifications.push({ + type: event.type, + guid: event.guid, + }); + } + } + break; + case "bookmark-moved": + notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + url: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + index: event.index, + oldParentGuid: event.oldParentGuid, + oldIndex: event.oldIndex, + isTagging: event.isTagging, + }); + break; + case "bookmark-tags-changed": + notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + url: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + tags: event.tags, + lastModified: new Date(event.lastModified), + source: event.source, + isTagging: event.isTagging, + }); + break; + case "bookmark-time-changed": + notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + url: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + dateAdded: new Date(event.dateAdded), + lastModified: new Date(event.lastModified), + source: event.source, + isTagging: event.isTagging, + }); + break; + case "bookmark-title-changed": + notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + url: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + title: event.title, + lastModified: new Date(event.lastModified), + source: event.source, + isTagging: event.isTagging, + }); + break; + case "bookmark-url-changed": + notifications.push({ + type: event.type, + id: event.id, + itemType: event.itemType, + url: event.url, + guid: event.guid, + parentGuid: event.parentGuid, + source: event.source, + isTagging: event.isTagging, + lastModified: new Date(event.lastModified), + }); + break; + } + } + }; + PlacesUtils.observers.addListener(types, listener); + return { + check(expectedNotifications) { + PlacesUtils.observers.removeListener(types, listener); + Assert.deepEqual(notifications, expectedNotifications); + }, + }; +} diff --git a/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js new file mode 100644 index 0000000000..3f74430296 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js @@ -0,0 +1,119 @@ +/* Bug 1016953 - When a previous bookmark backup exists with the same hash +regardless of date, an automatic backup should attempt to either rename it to +today's date if the backup was for an old date or leave it alone if it was for +the same date. However if the file ext was json it will accidentally rename it +to jsonlz4 while keeping the json contents +*/ + +add_task(async function test_same_date_same_hash() { + // If old file has been created on the same date and has the same hash + // the file should be left alone + let backupFolder = await PlacesBackups.getBackupFolder(); + // Save to profile dir to obtain hash and nodeCount to append to filename + let tempPath = PathUtils.join( + PathUtils.profileDir, + "bug10169583_bookmarks.json" + ); + let { count, hash } = await BookmarkJSONUtils.exportToFile(tempPath); + + // Save JSON file in backup folder with hash appended + let dateObj = new Date(); + let filename = + "bookmarks-" + + PlacesBackups.toISODateString(dateObj) + + "_" + + count + + "_" + + hash + + ".json"; + let backupFile = PathUtils.join(backupFolder, filename); + await IOUtils.move(tempPath, backupFile); + + // Force a compressed backup which fallbacks to rename + await PlacesBackups.create(); + let mostRecentBackupFile = await PlacesBackups.getMostRecentBackup(); + // check to ensure not renamed to jsonlz4 + Assert.equal(mostRecentBackupFile, backupFile); + // inspect contents and check if valid json + info("Check is valid JSON"); + // We initially wrote an uncompressed file, and although a backup was triggered + // it did not rewrite the file, so this is uncompressed. + await IOUtils.readJSON(mostRecentBackupFile); + + // Cleanup + await IOUtils.remove(backupFile); + await IOUtils.remove(tempPath); + PlacesBackups._backupFiles = null; // To force re-cache of backupFiles +}); + +add_task(async function test_same_date_diff_hash() { + // If the old file has been created on the same date, but has a different hash + // the existing file should be overwritten with the newer compressed version + let backupFolder = await PlacesBackups.getBackupFolder(); + let tempPath = PathUtils.join( + PathUtils.profileDir, + "bug10169583_bookmarks.json" + ); + let { count } = await BookmarkJSONUtils.exportToFile(tempPath); + let dateObj = new Date(); + let filename = + "bookmarks-" + + PlacesBackups.toISODateString(dateObj) + + "_" + + count + + "_differentHash==.json"; + let backupFile = PathUtils.join(backupFolder, filename); + await IOUtils.move(tempPath, backupFile); + await PlacesBackups.create(); // Force compressed backup + let mostRecentBackupFile = await PlacesBackups.getMostRecentBackup(); + + // Decode lz4 compressed file to json and check if json is valid + info("Check is valid JSON"); + await IOUtils.readJSON(mostRecentBackupFile, { decompress: true }); + + // Cleanup + await IOUtils.remove(mostRecentBackupFile); + await IOUtils.remove(tempPath); + PlacesBackups._backupFiles = null; // To force re-cache of backupFiles +}); + +add_task(async function test_diff_date_same_hash() { + // If the old file has been created on an older day but has the same hash + // it should be renamed with today's date without altering the contents. + let backupFolder = await PlacesBackups.getBackupFolder(); + let tempPath = PathUtils.join( + PathUtils.profileDir, + "bug10169583_bookmarks.json" + ); + let { count, hash } = await BookmarkJSONUtils.exportToFile(tempPath); + let oldDate = new Date(2014, 1, 1); + let curDate = new Date(); + let oldFilename = + "bookmarks-" + + PlacesBackups.toISODateString(oldDate) + + "_" + + count + + "_" + + hash + + ".json"; + let newFilename = + "bookmarks-" + + PlacesBackups.toISODateString(curDate) + + "_" + + count + + "_" + + hash + + ".json"; + let backupFile = PathUtils.join(backupFolder, oldFilename); + let newBackupFile = PathUtils.join(backupFolder, newFilename); + await IOUtils.move(tempPath, backupFile); + + // Ensure file has been renamed correctly + await PlacesBackups.create(); + let mostRecentBackupFile = await PlacesBackups.getMostRecentBackup(); + Assert.equal(mostRecentBackupFile, newBackupFile); + + // Cleanup + await IOUtils.remove(mostRecentBackupFile); + await IOUtils.remove(tempPath); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js new file mode 100644 index 0000000000..47955a4ea4 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js @@ -0,0 +1,117 @@ +/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 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/. */ + +/* Bug 1017502 - Add a foreign_count column to moz_places +This tests, tests the triggers that adjust the foreign_count when a bookmark is +added or removed and also the maintenance task to fix wrong counts. +*/ + +const T_URI = Services.io.newURI( + "https://www.mozilla.org/firefox/nightly/firstrun/" +); + +async function getForeignCountForURL(conn, url) { + await PlacesTestUtils.promiseAsyncUpdates(); + url = url instanceof Ci.nsIURI ? url.spec : url; + let rows = await conn.executeCached( + `SELECT foreign_count FROM moz_places WHERE url_hash = hash(:t_url) + AND url = :t_url`, + { t_url: url } + ); + return rows[0].getResultByName("foreign_count"); +} + +add_task(async function add_remove_change_bookmark_test() { + let conn = await PlacesUtils.promiseDBConnection(); + + // Simulate a visit to the url + await PlacesTestUtils.addVisits(T_URI); + Assert.equal(await getForeignCountForURL(conn, T_URI), 0); + + // Add 1st bookmark which should increment foreign_count by 1 + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "First Run", + url: T_URI, + }); + Assert.equal(await getForeignCountForURL(conn, T_URI), 1); + + // Add 2nd bookmark + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "First Run", + url: T_URI, + }); + Assert.equal(await getForeignCountForURL(conn, T_URI), 2); + + // Remove 2nd bookmark which should decrement foreign_count by 1 + await PlacesUtils.bookmarks.remove(bm2); + Assert.equal(await getForeignCountForURL(conn, T_URI), 1); + + // Change first bookmark's URI + const URI2 = Services.io.newURI("http://www.mozilla.org"); + bm1.url = URI2; + bm1 = await PlacesUtils.bookmarks.update(bm1); + // Check foreign count for original URI + Assert.equal(await getForeignCountForURL(conn, T_URI), 0); + // Check foreign count for new URI + Assert.equal(await getForeignCountForURL(conn, URI2), 1); + + // Cleanup - Remove changed bookmark + await PlacesUtils.bookmarks.remove(bm1); + Assert.equal(await getForeignCountForURL(conn, URI2), 0); +}); + +add_task(async function maintenance_foreign_count_test() { + let conn = await PlacesUtils.promiseDBConnection(); + + // Simulate a visit to the url + await PlacesTestUtils.addVisits(T_URI); + + // Adjust the foreign_count for the added entry to an incorrect value + await new Promise(resolve => { + let stmt = DBConn().createAsyncStatement( + `UPDATE moz_places SET foreign_count = 10 WHERE url_hash = hash(:t_url) + AND url = :t_url ` + ); + stmt.params.t_url = T_URI.spec; + stmt.executeAsync({ + handleCompletion() { + resolve(); + }, + }); + stmt.finalize(); + }); + Assert.equal(await getForeignCountForURL(conn, T_URI), 10); + + // Run maintenance + const { PlacesDBUtils } = ChromeUtils.importESModule( + "resource://gre/modules/PlacesDBUtils.sys.mjs" + ); + await PlacesDBUtils.maintenanceOnIdle(); + + // Check if the foreign_count has been adjusted to the correct value + Assert.equal(await getForeignCountForURL(conn, T_URI), 0); +}); + +add_task(async function add_remove_tags_test() { + let conn = await PlacesUtils.promiseDBConnection(); + + await PlacesTestUtils.addVisits(T_URI); + Assert.equal(await getForeignCountForURL(conn, T_URI), 0); + + // Check foreign count incremented by 1 for a single tag + PlacesUtils.tagging.tagURI(T_URI, ["test tag"]); + Assert.equal(await getForeignCountForURL(conn, T_URI), 1); + + // Check foreign count is incremented by 2 for two tags + PlacesUtils.tagging.tagURI(T_URI, ["one", "two"]); + Assert.equal(await getForeignCountForURL(conn, T_URI), 3); + + // Check foreign count is set to 0 when all tags are removed + PlacesUtils.tagging.untagURI(T_URI, ["test tag", "one", "two"]); + Assert.equal(await getForeignCountForURL(conn, T_URI), 0); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_1129529.js b/toolkit/components/places/tests/bookmarks/test_1129529.js new file mode 100644 index 0000000000..03f0840cf8 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_1129529.js @@ -0,0 +1,24 @@ +// Test that importing bookmark data where a bookmark has a tag longer than 100 +// chars imports everything except the tags for that bookmark. +add_task(async function() { + let bookmarksFile = PathUtils.join( + do_get_cwd().path, + "bookmarks_long_tag.json" + ); + let bookmarksUrl = PathUtils.toFileURI(bookmarksFile); + + await BookmarkJSONUtils.importFromURL(bookmarksUrl); + + let [bookmarks] = await PlacesBackups.getBookmarksTree(); + let unsortedBookmarks = bookmarks.children[2].children; + Assert.equal(unsortedBookmarks.length, 3); + + for (let i = 0; i < unsortedBookmarks.length; ++i) { + let bookmark = unsortedBookmarks[i]; + Assert.equal(bookmark.charset, "UTF-16"); + Assert.equal(bookmark.dateAdded, 1554906792000); + Assert.equal(bookmark.lastModified, 1554906792000); + Assert.equal(bookmark.uri, `http://test${i}.com/`); + Assert.equal(bookmark.tags, `tag${i}`); + } +}); diff --git a/toolkit/components/places/tests/bookmarks/test_384228.js b/toolkit/components/places/tests/bookmarks/test_384228.js new file mode 100644 index 0000000000..0db46353b7 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_384228.js @@ -0,0 +1,93 @@ +/* -*- 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/. */ + +/** + * test querying for bookmarks in multiple folders. + */ +add_task(async function search_bookmark_in_folder() { + let testFolder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 1", + }); + Assert.equal(testFolder1.index, 0); + + let testFolder2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 2", + }); + Assert.equal(testFolder2.index, 1); + + let testFolder3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 3", + }); + Assert.equal(testFolder3.index, 2); + + let b1 = await PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1.guid, + url: "http://foo.tld/", + title: "title b1 (folder 1)", + }); + Assert.equal(b1.index, 0); + + let b2 = await PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1.guid, + url: "http://foo.tld/", + title: "title b2 (folder 1)", + }); + Assert.equal(b2.index, 1); + + let b3 = await PlacesUtils.bookmarks.insert({ + parentGuid: testFolder2.guid, + url: "http://foo.tld/", + title: "title b3 (folder 2)", + }); + Assert.equal(b3.index, 0); + + let b4 = await PlacesUtils.bookmarks.insert({ + parentGuid: testFolder3.guid, + url: "http://foo.tld/", + title: "title b4 (folder 3)", + }); + Assert.equal(b4.index, 0); + + // also test recursive search + let testFolder1_1 = await PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 384228 test folder 1.1", + }); + Assert.equal(testFolder1_1.index, 2); + + let b5 = await PlacesUtils.bookmarks.insert({ + parentGuid: testFolder1_1.guid, + url: "http://foo.tld/", + title: "title b5 (folder 1.1)", + }); + Assert.equal(b5.index, 0); + + // query folder 1, folder 2 and get 4 bookmarks + let hs = PlacesUtils.history; + let options = hs.getNewQueryOptions(); + let query = hs.getNewQuery(); + query.searchTerms = "title"; + options.queryType = options.QUERY_TYPE_BOOKMARKS; + query.setParents([testFolder1.guid, testFolder2.guid]); + let rootNode = hs.executeQuery(query, options).root; + rootNode.containerOpen = true; + + // should not match item from folder 3 + Assert.equal(rootNode.childCount, 4); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(3).bookmarkGuid, b5.guid); + + rootNode.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_385829.js b/toolkit/components/places/tests/bookmarks/test_385829.js new file mode 100644 index 0000000000..9de7c6da17 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_385829.js @@ -0,0 +1,180 @@ +/* -*- 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/. */ + +add_task(async function search_bookmark_by_lastModified_dateDated() { + // test search on folder with various sorts and max results + // see bug #385829 for more details + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "bug 385829 test", + }); + + let now = new Date(); + // ensure some unique values for date added and last modified + // for date added: b1 < b2 < b3 < b4 + // for last modified: b1 > b2 > b3 > b4 + let b1 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a1.com/", + title: "1 title", + dateAdded: new Date(now.getTime() + 1000), + }); + let b2 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a2.com/", + title: "2 title", + dateAdded: new Date(now.getTime() + 2000), + }); + let b3 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a3.com/", + title: "3 title", + dateAdded: new Date(now.getTime() + 3000), + }); + let b4 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://a4.com/", + title: "4 title", + dateAdded: new Date(now.getTime() + 4000), + }); + + // make sure lastModified is larger than dateAdded + let modifiedTime = new Date(now.getTime() + 5000); + await PlacesUtils.bookmarks.update({ + guid: b1.guid, + lastModified: new Date(modifiedTime.getTime() + 4000), + }); + await PlacesUtils.bookmarks.update({ + guid: b2.guid, + lastModified: new Date(modifiedTime.getTime() + 3000), + }); + await PlacesUtils.bookmarks.update({ + guid: b3.guid, + lastModified: new Date(modifiedTime.getTime() + 2000), + }); + await PlacesUtils.bookmarks.update({ + guid: b4.guid, + lastModified: new Date(modifiedTime.getTime() + 1000), + }); + + let hs = PlacesUtils.history; + let options = hs.getNewQueryOptions(); + let query = hs.getNewQuery(); + options.queryType = options.QUERY_TYPE_BOOKMARKS; + options.maxResults = 3; + query.setParents([folder.guid]); + + let result = hs.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + + // test SORT_BY_DATEADDED_ASCENDING (live update) + result.sortingMode = options.SORT_BY_DATEADDED_ASCENDING; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok(rootNode.getChild(0).dateAdded < rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded < rootNode.getChild(2).dateAdded); + + // test SORT_BY_DATEADDED_DESCENDING (live update) + result.sortingMode = options.SORT_BY_DATEADDED_DESCENDING; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b1.guid); + Assert.ok(rootNode.getChild(0).dateAdded > rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded > rootNode.getChild(2).dateAdded); + + // test SORT_BY_LASTMODIFIED_ASCENDING (live update) + result.sortingMode = options.SORT_BY_LASTMODIFIED_ASCENDING; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b1.guid); + Assert.ok( + rootNode.getChild(0).lastModified < rootNode.getChild(1).lastModified + ); + Assert.ok( + rootNode.getChild(1).lastModified < rootNode.getChild(2).lastModified + ); + + // test SORT_BY_LASTMODIFIED_DESCENDING (live update) + result.sortingMode = options.SORT_BY_LASTMODIFIED_DESCENDING; + + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok( + rootNode.getChild(0).lastModified > rootNode.getChild(1).lastModified + ); + Assert.ok( + rootNode.getChild(1).lastModified > rootNode.getChild(2).lastModified + ); + rootNode.containerOpen = false; + + // test SORT_BY_DATEADDED_ASCENDING + options.sortingMode = options.SORT_BY_DATEADDED_ASCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok(rootNode.getChild(0).dateAdded < rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded < rootNode.getChild(2).dateAdded); + rootNode.containerOpen = false; + + // test SORT_BY_DATEADDED_DESCENDING + options.sortingMode = options.SORT_BY_DATEADDED_DESCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b4.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b2.guid); + Assert.ok(rootNode.getChild(0).dateAdded > rootNode.getChild(1).dateAdded); + Assert.ok(rootNode.getChild(1).dateAdded > rootNode.getChild(2).dateAdded); + rootNode.containerOpen = false; + + // test SORT_BY_LASTMODIFIED_ASCENDING + options.sortingMode = options.SORT_BY_LASTMODIFIED_ASCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b4.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b3.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b2.guid); + Assert.ok( + rootNode.getChild(0).lastModified < rootNode.getChild(1).lastModified + ); + Assert.ok( + rootNode.getChild(1).lastModified < rootNode.getChild(2).lastModified + ); + rootNode.containerOpen = false; + + // test SORT_BY_LASTMODIFIED_DESCENDING + options.sortingMode = options.SORT_BY_LASTMODIFIED_DESCENDING; + result = hs.executeQuery(query, options); + rootNode = result.root; + rootNode.containerOpen = true; + Assert.equal(rootNode.childCount, 3); + Assert.equal(rootNode.getChild(0).bookmarkGuid, b1.guid); + Assert.equal(rootNode.getChild(1).bookmarkGuid, b2.guid); + Assert.equal(rootNode.getChild(2).bookmarkGuid, b3.guid); + Assert.ok( + rootNode.getChild(0).lastModified > rootNode.getChild(1).lastModified + ); + Assert.ok( + rootNode.getChild(1).lastModified > rootNode.getChild(2).lastModified + ); + rootNode.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_388695.js b/toolkit/components/places/tests/bookmarks/test_388695.js new file mode 100644 index 0000000000..337d8176bd --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_388695.js @@ -0,0 +1,45 @@ +/* -*- 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/. */ + +// Get bookmark service +let bm = PlacesUtils.bookmarks; + +// Test that Bookmarks fetch properly orders its results based on +// the last modified value. Note we cannot rely on dateAdded due to +// the low PR_Now() resolution. + +add_task(async function sort_bookmark_by_relevance() { + let now = new Date(); + let modifiedTime = new Date(now.setHours(now.getHours() - 2)); + + let url = "http://foo.tld.com/"; + let parentGuid = ( + await bm.insert({ + type: bm.TYPE_FOLDER, + title: "test folder", + parentGuid: bm.unfiledGuid, + }) + ).guid; + let item1Guid = (await bm.insert({ url, parentGuid })).guid; + let item2Guid = ( + await bm.insert({ + url, + parentGuid, + dateAdded: modifiedTime, + lastModified: modifiedTime, + }) + ).guid; + let bms = []; + await bm.fetch({ url }, bm1 => bms.push(bm1)); + Assert.equal(bms[0].guid, item1Guid); + Assert.equal(bms[1].guid, item2Guid); + await bm.update({ guid: item2Guid, title: "modified" }); + + let bms1 = []; + await bm.fetch({ url }, bm2 => bms1.push(bm2)); + Assert.equal(bms1[0].guid, item2Guid); + Assert.equal(bms1[1].guid, item1Guid); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_393498.js b/toolkit/components/places/tests/bookmarks/test_393498.js new file mode 100644 index 0000000000..1f9d21ce67 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_393498.js @@ -0,0 +1,170 @@ +/* -*- 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/. */ + +var observer = { + handlePlacesEvents(events) { + for (const event of events) { + switch (event.type) { + case "bookmark-added": { + this._itemAddedId = event.id; + this._itemAddedParent = event.parentId; + this._itemAddedIndex = event.index; + break; + } + case "bookmark-time-changed": { + this._itemTimeChangedGuid = event.guid; + this._itemTimeChangedDateAdded = event.dateAdded; + this._itemTimeChangedLastModified = event.lastModified; + break; + } + case "bookmark-title-changed": { + this._itemTitleChangedId = event.id; + this._itemTitleChangedTitle = event.title; + break; + } + } + } + }, + onItemChanged(id, property, isAnnotationProperty, value) { + this._itemChangedId = id; + this._itemChangedProperty = property; + this._itemChanged_isAnnotationProperty = isAnnotationProperty; + this._itemChangedValue = value; + }, +}; +Object.setPrototypeOf(observer, NavBookmarkObserver.prototype); + +PlacesUtils.bookmarks.addObserver(observer); +observer.handlePlacesEvents = observer.handlePlacesEvents.bind(observer); +PlacesUtils.observers.addListener( + ["bookmark-added", "bookmark-time-changed", "bookmark-title-changed"], + observer.handlePlacesEvents +); + +registerCleanupFunction(function() { + PlacesUtils.bookmarks.removeObserver(observer); + PlacesUtils.observers.removeListener( + ["bookmark-added", "bookmark-time-changed", "bookmark-title-changed"], + observer.handlePlacesEvents + ); +}); + +// Returns do_check_eq with .getTime() added onto parameters +function do_check_date_eq(t1, t2) { + return Assert.equal(t1.getTime(), t2.getTime()); +} + +add_task(async function test_bookmark_update_notifications() { + // We set times in the past to workaround a timing bug due to virtual + // machines and the skew between PR_Now() and Date.now(), see bug 427142 and + // bug 858377 for details. + const PAST_DATE = new Date(Date.now() - 86400000); + + // Insert a new bookmark. + let testFolder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "test Folder", + parentGuid: PlacesUtils.bookmarks.menuGuid, + }); + + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: testFolder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://google.com/", + title: "a bookmark", + }); + + // Sanity check. + Assert.ok(observer.itemChangedProperty === undefined); + + // Set dateAdded in the past and verify the changes. + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + dateAdded: PAST_DATE, + }); + + Assert.equal(observer._itemTimeChangedGuid, bookmark.guid); + Assert.equal(observer._itemTimeChangedDateAdded, PAST_DATE.getTime()); + + // After just inserting, modified should be the same as dateAdded. + do_check_date_eq(bookmark.lastModified, bookmark.dateAdded); + + let updatedBookmark = await PlacesUtils.bookmarks.fetch({ + guid: bookmark.guid, + }); + + do_check_date_eq(updatedBookmark.dateAdded, PAST_DATE); + + // Set lastModified in the past and verify the changes. + updatedBookmark = await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + lastModified: PAST_DATE, + }); + + Assert.equal(observer._itemTimeChangedGuid, bookmark.guid); + Assert.equal(observer._itemTimeChangedLastModified, PAST_DATE.getTime()); + do_check_date_eq(updatedBookmark.lastModified, PAST_DATE); + + // Set bookmark title + updatedBookmark = await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + title: "Google", + }); + + // Test notifications. + Assert.equal( + observer._itemTitleChangedId, + await PlacesUtils.promiseItemId(bookmark.guid) + ); + Assert.equal(observer._itemTitleChangedTitle, "Google"); + + // Check lastModified has been updated. + Assert.ok(is_time_ordered(PAST_DATE, updatedBookmark.lastModified.getTime())); + + // Check that node properties are updated. + let root = PlacesUtils.getFolderContents(testFolder.guid).root; + Assert.equal(root.childCount, 1); + let childNode = root.getChild(0); + + // confirm current dates match node properties + Assert.equal( + PlacesUtils.toPRTime(updatedBookmark.dateAdded), + childNode.dateAdded + ); + Assert.equal( + PlacesUtils.toPRTime(updatedBookmark.lastModified), + childNode.lastModified + ); + + // Test live update of lastModified when setting title. + updatedBookmark = await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + lastModified: PAST_DATE, + title: "Google", + }); + + // Check lastModified has been updated. + Assert.ok(is_time_ordered(PAST_DATE, childNode.lastModified)); + // Test that node value matches db value. + Assert.equal( + PlacesUtils.toPRTime(updatedBookmark.lastModified), + childNode.lastModified + ); + + // Test live update of the exposed date apis. + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + dateAdded: PAST_DATE, + }); + Assert.equal(childNode.dateAdded, PlacesUtils.toPRTime(PAST_DATE)); + await PlacesUtils.bookmarks.update({ + guid: bookmark.guid, + lastModified: PAST_DATE, + }); + Assert.equal(childNode.lastModified, PlacesUtils.toPRTime(PAST_DATE)); + + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js new file mode 100644 index 0000000000..de532b0361 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js @@ -0,0 +1,253 @@ +/* -*- 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/. */ + +var tests = []; + +/* + +Backup/restore tests example: + +var myTest = { + populate: function () { ... add bookmarks ... }, + validate: function () { ... query for your bookmarks ... } +} + +this.push(myTest); + +*/ + +/* + +test summary: +- create folders with content +- create a query bookmark for those folders +- backs up bookmarks +- restores bookmarks +- confirms that the query has the new ids for the same folders + +scenarios: +- 1 folder (folder shortcut) +- n folders (single query) +- n folders (multiple queries) + +*/ + +var test = { + _testRootId: null, + _testRootTitle: "test root", + _folderGuids: [], + _bookmarkURIs: [], + _count: 3, + _extraBookmarksCount: 10, + + populate: async function populate() { + // folder to hold this test + await PlacesUtils.bookmarks.eraseEverything(); + + let testFolderItems = []; + // Set a date 60 seconds ago, so that we can set newer bookmarks later. + let dateAdded = new Date(new Date() - 60000); + + // create test folders each with a bookmark + for (let i = 0; i < this._count; i++) { + this._folderGuids.push(PlacesUtils.history.makeGuid()); + testFolderItems.push({ + guid: this._folderGuids[i], + title: `folder${i}`, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + dateAdded, + children: [ + { + dateAdded, + url: `http://${i}`, + title: `bookmark${i}`, + }, + ], + }); + } + + let bookmarksTree = { + guid: PlacesUtils.bookmarks.toolbarGuid, + children: [ + { + dateAdded, + title: this._testRootTitle, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: testFolderItems, + }, + ], + }; + + let insertedBookmarks = await PlacesUtils.bookmarks.insertTree( + bookmarksTree + ); + + // create a query URI with 1 folder (ie: folder shortcut) + this._queryURI1 = `place:parent=${this._folderGuids[0]}&queryType=1`; + this._queryTitle1 = "query1"; + await PlacesUtils.bookmarks.insert({ + parentGuid: insertedBookmarks[0].guid, + dateAdded, + url: this._queryURI1, + title: this._queryTitle1, + }); + + // create a query URI with _count folders + this._queryURI2 = `place:parent=${this._folderGuids.join( + "&parent=" + )}&queryType=1`; + this._queryTitle2 = "query2"; + await PlacesUtils.bookmarks.insert({ + parentGuid: insertedBookmarks[0].guid, + dateAdded, + url: this._queryURI2, + title: this._queryTitle2, + }); + + // Create a query URI for most recent bookmarks with NO folders specified. + this._queryURI3 = + "place:queryType=1&sort=12&maxResults=10&excludeQueries=1"; + this._queryTitle3 = "query3"; + await PlacesUtils.bookmarks.insert({ + parentGuid: insertedBookmarks[0].guid, + dateAdded, + url: this._queryURI3, + title: this._queryTitle3, + }); + }, + + clean() {}, + + validate: async function validate(addExtras) { + if (addExtras) { + // Throw a wrench in the works by inserting some new bookmarks, + // ensuring folder ids won't be the same, when restoring. + let date = new Date() - this._extraBookmarksCount * 1000; + for (let i = 0; i < this._extraBookmarksCount; i++) { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: uri("http://aaaa" + i), + dateAdded: new Date(date + (this._extraBookmarksCount - i) * 1000), + }); + } + } + + var toolbar = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid, + false, + true + ).root; + Assert.equal(toolbar.childCount, 1); + + var folderNode = toolbar.getChild(0); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + Assert.equal(folderNode.title, this._testRootTitle); + folderNode.QueryInterface(Ci.nsINavHistoryQueryResultNode); + folderNode.containerOpen = true; + + // |_count| folders + the query nodes + Assert.equal(folderNode.childCount, this._count + 3); + + for (let i = 0; i < this._count; i++) { + var subFolder = folderNode.getChild(i); + Assert.equal(subFolder.title, "folder" + i); + subFolder.QueryInterface(Ci.nsINavHistoryContainerResultNode); + subFolder.containerOpen = true; + Assert.equal(subFolder.childCount, 1); + var child = subFolder.getChild(0); + Assert.equal(child.title, "bookmark" + i); + Assert.ok(uri(child.uri).equals(uri("http://" + i))); + } + + // validate folder shortcut + this.validateQueryNode1(folderNode.getChild(this._count)); + + // validate folders query + this.validateQueryNode2(folderNode.getChild(this._count + 1)); + + // validate recent folders query + this.validateQueryNode3(folderNode.getChild(this._count + 2)); + + // clean up + folderNode.containerOpen = false; + toolbar.containerOpen = false; + }, + + validateQueryNode1: function validateQueryNode1(aNode) { + Assert.equal(aNode.title, this._queryTitle1); + Assert.ok(PlacesUtils.nodeIsFolder(aNode)); + + aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + aNode.containerOpen = true; + Assert.equal(aNode.childCount, 1); + var child = aNode.getChild(0); + Assert.ok(uri(child.uri).equals(uri("http://0"))); + Assert.equal(child.title, "bookmark0"); + aNode.containerOpen = false; + }, + + validateQueryNode2: function validateQueryNode2(aNode) { + Assert.equal(aNode.title, this._queryTitle2); + Assert.ok(PlacesUtils.nodeIsQuery(aNode)); + + aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + aNode.containerOpen = true; + Assert.equal(aNode.childCount, this._count); + for (var i = 0; i < aNode.childCount; i++) { + var child = aNode.getChild(i); + Assert.ok(uri(child.uri).equals(uri("http://" + i))); + Assert.equal(child.title, "bookmark" + i); + } + aNode.containerOpen = false; + }, + + validateQueryNode3(aNode) { + Assert.equal(aNode.title, this._queryTitle3); + Assert.ok(PlacesUtils.nodeIsQuery(aNode)); + + aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + aNode.containerOpen = true; + // The query will list the extra bookmarks added at the start of validate. + Assert.equal(aNode.childCount, this._extraBookmarksCount); + for (var i = 0; i < aNode.childCount; i++) { + var child = aNode.getChild(i); + Assert.equal(child.uri, `http://aaaa${i}/`); + } + aNode.containerOpen = false; + }, +}; +tests.push(test); + +add_task(async function() { + // make json file + let jsonFile = PathUtils.join(PathUtils.profileDir, "bookmarks.json"); + + // populate db + for (let singleTest of tests) { + await singleTest.populate(); + // sanity + await singleTest.validate(true); + } + + // export json to file + await BookmarkJSONUtils.exportToFile(jsonFile); + + // clean + for (let singleTest of tests) { + singleTest.clean(); + } + + // restore json file + await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true }); + + // validate + for (let singleTest of tests) { + await singleTest.validate(false); + } + + // clean up + await IOUtils.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js new file mode 100644 index 0000000000..73a7023e45 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js @@ -0,0 +1,47 @@ +/* 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/. */ + +"use strict"; + +const FOLDER_TITLE = '"quoted folder"'; + +function checkQuotedFolder() { + let toolbar = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.toolbarGuid) + .root; + + // test for our quoted folder + Assert.equal(toolbar.childCount, 1); + var folderNode = toolbar.getChild(0); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER); + Assert.equal(folderNode.title, FOLDER_TITLE); + + // clean up + toolbar.containerOpen = false; +} + +add_task(async function() { + // make json file + let jsonFile = PathUtils.join(PathUtils.profileDir, "bookmarks.json"); + + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: FOLDER_TITLE, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + checkQuotedFolder(); + + // export json to file + await BookmarkJSONUtils.exportToFile(jsonFile); + + await PlacesUtils.bookmarks.remove(folder.guid); + + // restore json file + await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true }); + + checkQuotedFolder(); + + // clean up + await IOUtils.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_448584.js b/toolkit/components/places/tests/bookmarks/test_448584.js new file mode 100644 index 0000000000..5bfb0eb73c --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_448584.js @@ -0,0 +1,90 @@ +/* -*- 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/. */ + +// Get database connection +try { + var mDBConn = PlacesUtils.history.DBConnection; +} catch (ex) { + do_throw("Could not get database connection\n"); +} + +/* + This test is: + - don't try to add invalid uri nodes to a JSON backup +*/ + +const ITEM_TITLE = "invalid uri"; +const ITEM_URL = "http://test.mozilla.org"; + +function validateResults(expectedValidItemsCount) { + var query = PlacesUtils.history.getNewQuery(); + query.setParents([PlacesUtils.bookmarks.toolbarGuid]); + var options = PlacesUtils.history.getNewQueryOptions(); + var result = PlacesUtils.history.executeQuery(query, options); + + var toolbar = result.root; + toolbar.containerOpen = true; + + // test for our bookmark + Assert.equal(toolbar.childCount, expectedValidItemsCount); + for (var i = 0; i < toolbar.childCount; i++) { + var folderNode = toolbar.getChild(0); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_URI); + Assert.equal(folderNode.title, ITEM_TITLE); + } + + // clean up + toolbar.containerOpen = false; +} + +add_task(async function() { + // make json file + let jsonFile = PathUtils.join(PathUtils.profileDir, "bookmarks.json"); + + // populate db + // add a valid bookmark + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: ITEM_TITLE, + url: ITEM_URL, + }); + + let badBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: ITEM_TITLE, + url: ITEM_URL, + }); + // sanity + validateResults(2); + // Something in the code went wrong and we finish up losing the place, so + // the bookmark uri becomes null. + var sql = "UPDATE moz_bookmarks SET fk = 1337 WHERE guid = ?1"; + var stmt = mDBConn.createStatement(sql); + stmt.bindByIndex(0, badBookmark.guid); + try { + stmt.execute(); + } finally { + stmt.finalize(); + } + + await BookmarkJSONUtils.exportToFile(jsonFile); + + // clean + await PlacesUtils.bookmarks.remove(badBookmark); + + // restore json file + try { + await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true }); + } catch (ex) { + do_throw("couldn't import the exported file: " + ex); + } + + // validate + validateResults(1); + + // clean up + await IOUtils.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_458683.js b/toolkit/components/places/tests/bookmarks/test_458683.js new file mode 100644 index 0000000000..05dd5abe49 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_458683.js @@ -0,0 +1,109 @@ +/* -*- 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/. */ + +/* + This test is: + - don't block while doing backup and restore if tag containers contain + bogus items (separators, folders) +*/ + +const ITEM_TITLE = "invalid uri"; +const ITEM_URL = "http://test.mozilla.org/"; +const TAG_NAME = "testTag"; + +function validateResults() { + let toolbar = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.toolbarGuid) + .root; + // test for our bookmark + Assert.equal(toolbar.childCount, 1); + for (var i = 0; i < toolbar.childCount; i++) { + var folderNode = toolbar.getChild(0); + Assert.equal(folderNode.type, folderNode.RESULT_TYPE_URI); + Assert.equal(folderNode.title, ITEM_TITLE); + } + toolbar.containerOpen = false; + + // test for our tag + var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(ITEM_URL)); + Assert.equal(tags.length, 1); + Assert.equal(tags[0], TAG_NAME); +} + +add_task(async function() { + let jsonFile = PathUtils.join(PathUtils.profileDir, "bookmarks.json"); + + // add a valid bookmark + let item = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: ITEM_TITLE, + url: ITEM_URL, + }); + + // create a tag + PlacesUtils.tagging.tagURI(Services.io.newURI(ITEM_URL), [TAG_NAME]); + // get tag folder id + let tagRoot = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.tagsGuid) + .root; + Assert.equal(tagRoot.childCount, 1); + let tagItemGuid = PlacesUtils.asContainer(tagRoot.getChild(0)).bookmarkGuid; + tagRoot.containerOpen = false; + + function insert({ type, parentGuid }) { + return PlacesUtils.withConnectionWrapper( + "test_458683: insert", + async db => { + await db.executeCached( + `INSERT INTO moz_bookmarks (type, parent, position, guid) + VALUES (:type, + (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid), + (SELECT MAX(position) + 1 FROM moz_bookmarks WHERE parent = (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid)), + GENERATE_GUID())`, + { type, parentGuid } + ); + } + ); + } + + // add a separator and a folder inside tag folder + // We must insert these manually, because the new bookmarking API doesn't + // support inserting invalid items into the tag folder. + await insert({ + parentGuid: tagItemGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + await insert({ + parentGuid: tagItemGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + // add a separator and a folder inside tag root + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.tagsGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: "test tags root folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + // sanity + validateResults(); + + await BookmarkJSONUtils.exportToFile(jsonFile); + + // clean + PlacesUtils.tagging.untagURI(Services.io.newURI(ITEM_URL), [TAG_NAME]); + await PlacesUtils.bookmarks.remove(item); + + // restore json file + await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true }); + + validateResults(); + + // clean up + await IOUtils.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js new file mode 100644 index 0000000000..4596ed93b2 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js @@ -0,0 +1,86 @@ +/* -*- 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/. */ + +// Since PlacesBackups.getbackupFiles() is a lazy getter, these tests must +// run in the given order, to avoid making it out-of-sync. + +async function countChildren(path) { + let children = await IOUtils.getChildren(path); + let count = 0; + let lastBackupPath = null; + for (let entry of children) { + count++; + if (PlacesBackups.filenamesRegex.test(PathUtils.filename(entry))) { + lastBackupPath = entry; + } + } + return { count, lastBackupPath }; +} + +add_task(async function check_max_backups_is_respected() { + // Get bookmarkBackups directory + let backupFolder = await PlacesBackups.getBackupFolder(); + + // Create 2 json dummy backups in the past. + let oldJsonPath = PathUtils.join(backupFolder, "bookmarks-2008-01-01.json"); + await IOUtils.writeUTF8(oldJsonPath, ""); + Assert.ok(await IOUtils.exists(oldJsonPath)); + + let jsonPath = PathUtils.join(backupFolder, "bookmarks-2008-01-31.json"); + await IOUtils.writeUTF8(jsonPath, ""); + Assert.ok(await IOUtils.exists(jsonPath)); + + // Export bookmarks to JSON. + // Allow 2 backups, the older one should be removed. + await PlacesBackups.create(2); + + let { count, lastBackupPath } = await countChildren(backupFolder); + Assert.equal(count, 2); + Assert.notEqual(lastBackupPath, null); + Assert.equal(false, await IOUtils.exists(oldJsonPath)); + Assert.ok(await IOUtils.exists(jsonPath)); +}); + +add_task(async function check_max_backups_greater_than_backups() { + // Get bookmarkBackups directory + let backupFolder = await PlacesBackups.getBackupFolder(); + + // Export bookmarks to JSON. + // Allow 3 backups, none should be removed. + await PlacesBackups.create(3); + + let { count, lastBackupPath } = await countChildren(backupFolder); + Assert.equal(count, 2); + Assert.notEqual(lastBackupPath, null); +}); + +add_task(async function check_max_backups_null() { + // Get bookmarkBackups directory + let backupFolder = await PlacesBackups.getBackupFolder(); + + // Export bookmarks to JSON. + // Allow infinite backups, none should be removed, a new one is not created + // since one for today already exists. + await PlacesBackups.create(null); + + let { count, lastBackupPath } = await countChildren(backupFolder); + Assert.equal(count, 2); + Assert.notEqual(lastBackupPath, null); +}); + +add_task(async function check_max_backups_undefined() { + // Get bookmarkBackups directory + let backupFolder = await PlacesBackups.getBackupFolder(); + + // Export bookmarks to JSON. + // Allow infinite backups, none should be removed, a new one is not created + // since one for today already exists. + await PlacesBackups.create(); + + let { count, lastBackupPath } = await countChildren(backupFolder); + Assert.equal(count, 2); + Assert.notEqual(lastBackupPath, null); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js new file mode 100644 index 0000000000..ab4f4a02d5 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js @@ -0,0 +1,56 @@ +/* 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/. */ + +"use strict"; + +add_task(async function test_json_backup_in_future() { + let backupFolder = await PlacesBackups.getBackupFolder(); + let bookmarksBackupDir = new FileUtils.File(backupFolder); + // Remove all files from backups folder. + let files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.nextFile; + entry.remove(false); + } + + // Create a json dummy backup in the future. + let dateObj = new Date(); + dateObj.setYear(dateObj.getFullYear() + 1); + let name = PlacesBackups.getFilenameForDate(dateObj); + Assert.equal( + name, + "bookmarks-" + PlacesBackups.toISODateString(dateObj) + ".json" + ); + files = bookmarksBackupDir.directoryEntries; + while (files.hasMoreElements()) { + let entry = files.nextFile; + if (PlacesBackups.filenamesRegex.test(entry.leafName)) { + entry.remove(false); + } + } + + let futureBackupFile = bookmarksBackupDir.clone(); + futureBackupFile.append(name); + futureBackupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0o600); + Assert.ok(futureBackupFile.exists()); + + Assert.equal((await PlacesBackups.getBackupFiles()).length, 0); + + await PlacesBackups.create(); + // Check that a backup for today has been created. + Assert.equal((await PlacesBackups.getBackupFiles()).length, 1); + let mostRecentBackupFile = await PlacesBackups.getMostRecentBackup(); + Assert.notEqual(mostRecentBackupFile, null); + Assert.ok( + PlacesBackups.filenamesRegex.test(PathUtils.filename(mostRecentBackupFile)) + ); + + // Check that future backup has been removed. + Assert.ok(!futureBackupFile.exists()); + + // Cleanup. + mostRecentBackupFile = new FileUtils.File(mostRecentBackupFile); + mostRecentBackupFile.remove(false); + Assert.ok(!mostRecentBackupFile.exists()); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js b/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js new file mode 100644 index 0000000000..c58b5a7d8a --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js @@ -0,0 +1,66 @@ +/* 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/. */ + +/** + * Checks that automatically created bookmark backups are discarded if they are + * duplicate of an existing ones. + */ +add_task(async function() { + // Create a backup for yesterday in the backups folder. + let backupFolder = await PlacesBackups.getBackupFolder(); + let dateObj = new Date(); + dateObj.setDate(dateObj.getDate() - 1); + let oldBackupName = PlacesBackups.getFilenameForDate(dateObj); + let oldBackup = PathUtils.join(backupFolder, oldBackupName); + let { count: count, hash: hash } = await BookmarkJSONUtils.exportToFile( + oldBackup + ); + Assert.ok(count > 0); + Assert.equal(hash.length, 24); + oldBackupName = oldBackupName.replace( + /\.json/, + "_" + count + "_" + hash + ".json" + ); + await IOUtils.move(oldBackup, PathUtils.join(backupFolder, oldBackupName)); + + // Create a backup. + // This should just rename the existing backup, so in the end there should be + // only one backup with today's date. + await PlacesBackups.create(); + + // Get the hash of the generated backup + let backupFiles = await PlacesBackups.getBackupFiles(); + Assert.equal(backupFiles.length, 1); + + let matches = PathUtils.filename(backupFiles[0]).match( + PlacesBackups.filenamesRegex + ); + Assert.equal(matches[1], PlacesBackups.toISODateString(new Date())); + Assert.equal(matches[2], count); + Assert.equal(matches[3], hash); + + // Add a bookmark and create another backup. + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "foo", + url: "http://foo.com", + }); + + // We must enforce a backup since one for today already exists. The forced + // backup will replace the existing one. + await PlacesBackups.create(undefined, true); + Assert.equal(backupFiles.length, 1); + let recentBackup = await PlacesBackups.getMostRecentBackup(); + Assert.notEqual(recentBackup, PathUtils.join(backupFolder, oldBackupName)); + matches = PathUtils.filename(recentBackup).match( + PlacesBackups.filenamesRegex + ); + Assert.equal(matches[1], PlacesBackups.toISODateString(new Date())); + Assert.equal(matches[2], count + 1); + Assert.notEqual(matches[3], hash); + + // Clean up + await PlacesUtils.bookmarks.remove(bookmark); + await PlacesBackups.create(0); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js new file mode 100644 index 0000000000..608ce29b0a --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js @@ -0,0 +1,60 @@ +/* -*- 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/. */ + +add_task(async function compress_bookmark_backups_test() { + // Check for jsonlz4 extension + let todayFilename = PlacesBackups.getFilenameForDate( + new Date(2014, 4, 15), + true + ); + Assert.equal(todayFilename, "bookmarks-2014-05-15.jsonlz4"); + + await PlacesBackups.create(); + + // Check that a backup for today has been created and the regex works fine for lz4. + Assert.equal((await PlacesBackups.getBackupFiles()).length, 1); + let mostRecentBackupFile = await PlacesBackups.getMostRecentBackup(); + Assert.notEqual(mostRecentBackupFile, null); + Assert.ok( + PlacesBackups.filenamesRegex.test(PathUtils.filename(mostRecentBackupFile)) + ); + + // The most recent backup file has to be removed since saveBookmarksToJSONFile + // will otherwise over-write the current backup, since it will be made on the + // same date + await IOUtils.remove(mostRecentBackupFile); + Assert.equal(false, await IOUtils.exists(mostRecentBackupFile)); + + // Check that, if the user created a custom backup out of the default + // backups folder, it gets copied (compressed) into it. + let jsonFile = PathUtils.join(PathUtils.profileDir, "bookmarks.json"); + await PlacesBackups.saveBookmarksToJSONFile(jsonFile); + Assert.equal((await PlacesBackups.getBackupFiles()).length, 1); + + // Check if import works from lz4 compressed json + let url = "http://www.mozilla.org/en-US/"; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark", + url, + }); + + // Force create a compressed backup, Remove the bookmark, the restore the backup + await PlacesBackups.create(undefined, true); + let recentBackup = await PlacesBackups.getMostRecentBackup(); + await PlacesUtils.bookmarks.remove(bm); + await BookmarkJSONUtils.importFromFile(recentBackup, { replace: true }); + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.unfiledGuid) + .root; + let node = root.getChild(0); + Assert.equal(node.uri, url); + + root.containerOpen = false; + await PlacesUtils.bookmarks.eraseEverything(); + + // Cleanup. + await IOUtils.remove(jsonFile); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js new file mode 100644 index 0000000000..6d280e8cad --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js @@ -0,0 +1,53 @@ +/* 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/. */ + +/** + * To confirm that metadata i.e. bookmark count is set and retrieved for + * automatic backups. + */ +add_task(async function test_saveBookmarksToJSONFile_and_create() { + // Add a bookmark + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "Get Firefox!", + url: "http://getfirefox.com/", + }); + + // Test saveBookmarksToJSONFile() + let backupFile = PathUtils.join(PathUtils.tempDir, "bookmarks.json"); + + let nodeCount = await PlacesBackups.saveBookmarksToJSONFile(backupFile, true); + Assert.ok(nodeCount > 0); + Assert.ok(await IOUtils.exists(backupFile)); + + // Ensure the backup would be copied to our backups folder when the original + // backup is saved somewhere else. + let recentBackup = await PlacesBackups.getMostRecentBackup(); + let matches = PathUtils.filename(recentBackup).match( + PlacesBackups.filenamesRegex + ); + Assert.equal(matches[2], nodeCount); + Assert.equal(matches[3].length, 24); + + // Clear all backups in our backups folder. + await PlacesBackups.create(0); + Assert.equal((await PlacesBackups.getBackupFiles()).length, 0); + + // Test create() which saves bookmarks with metadata on the filename. + await PlacesBackups.create(); + Assert.equal((await PlacesBackups.getBackupFiles()).length, 1); + + let mostRecentBackupFile = await PlacesBackups.getMostRecentBackup(); + Assert.notEqual(mostRecentBackupFile, null); + matches = PathUtils.filename(recentBackup).match( + PlacesBackups.filenamesRegex + ); + Assert.equal(matches[2], nodeCount); + Assert.equal(matches[3].length, 24); + + // Cleanup + await IOUtils.remove(backupFile); + await PlacesBackups.create(0); + await PlacesUtils.bookmarks.remove(bookmark); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js new file mode 100644 index 0000000000..e1aa50b60d --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js @@ -0,0 +1,64 @@ +/* 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/. */ + +/** + * Checks that backups properly include all of the bookmarks if the hierarchy + * in the database is unordered so that a hierarchy is defined before its + * ancestor in the bookmarks table. + */ +add_task(async function() { + let bms = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "bookmark", + url: "http://mozilla.org", + }, + { + title: "f2", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "f1", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }); + + let bookmark = bms[0]; + let folder2 = bms[1]; + let folder1 = bms[2]; + bookmark.parentGuid = folder2.guid; + await PlacesUtils.bookmarks.update(bookmark); + + folder2.parentGuid = folder1.guid; + await PlacesUtils.bookmarks.update(folder2); + + // Create a backup. + await PlacesBackups.create(); + + // Remove the bookmarks, then restore the backup. + await PlacesUtils.bookmarks.remove(folder1); + await BookmarkJSONUtils.importFromFile( + await PlacesBackups.getMostRecentBackup(), + { replace: true } + ); + + info("Checking first level"); + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.unfiledGuid) + .root; + let level1 = root.getChild(0); + Assert.equal(level1.title, "f1"); + info("Checking second level"); + PlacesUtils.asContainer(level1).containerOpen = true; + let level2 = level1.getChild(0); + Assert.equal(level2.title, "f2"); + info("Checking bookmark"); + PlacesUtils.asContainer(level2).containerOpen = true; + bookmark = level2.getChild(0); + Assert.equal(bookmark.title, "bookmark"); + level2.containerOpen = false; + level1.containerOpen = false; + root.containerOpen = false; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js new file mode 100644 index 0000000000..a4d311494b --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js @@ -0,0 +1,36 @@ +/* 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/. */ + +/** + * Checks that we don't encodeURI twice when creating bookmarks.html. + */ +add_task(async function() { + let url = + "http://bt.ktxp.com/search.php?keyword=%E5%A6%84%E6%83%B3%E5%AD%A6%E7%94%9F%E4%BC%9A"; + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "bookmark", + url, + }); + + let file = PathUtils.join( + PathUtils.profileDir, + "bookmarks.exported.997030.html" + ); + await IOUtils.remove(file, { ignoreAbsent: true }); + await BookmarkHTMLUtils.exportToFile(file); + + // Remove the bookmarks, then restore the backup. + await PlacesUtils.bookmarks.remove(bm); + await BookmarkHTMLUtils.importFromFile(file, { replace: true }); + + info("Checking first level"); + let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.unfiledGuid) + .root; + let node = root.getChild(0); + Assert.equal(node.uri, url); + + root.containerOpen = false; + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_async_observers.js b/toolkit/components/places/tests/bookmarks/test_async_observers.js new file mode 100644 index 0000000000..641e972cc5 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_async_observers.js @@ -0,0 +1,85 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/* This test checks that bookmarks service is correctly forwarding async + * events like visit or favicon additions. */ + +let gBookmarkGuids = []; + +add_task(async function setup() { + // Add multiple bookmarks to the same uri. + gBookmarkGuids.push( + ( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://book.ma.rk/", + }) + ).guid + ); + gBookmarkGuids.push( + ( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://book.ma.rk/", + }) + ).guid + ); + Assert.equal(gBookmarkGuids.length, 2); +}); + +add_task(async function test_add_icon() { + // Add a visit to the bookmark and wait for the observer. + let guids = new Set(gBookmarkGuids); + Assert.equal(guids.size, 2); + let promiseNotifications = PlacesTestUtils.waitForNotification( + "favicon-changed", + events => + events.some( + event => + event.url == "http://book.ma.rk/" && + event.faviconUrl.startsWith("data:image/png;base64") + ), + "places" + ); + + PlacesUtils.favicons.setAndFetchFaviconForPage( + NetUtil.newURI("http://book.ma.rk/"), + SMALLPNG_DATA_URI, + true, + PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE, + null, + Services.scriptSecurityManager.getSystemPrincipal() + ); + await promiseNotifications; +}); + +add_task(async function test_remove_page() { + // Add a visit to the bookmark and wait for the observer. + let guids = new Set(gBookmarkGuids); + Assert.equal(guids.size, 2); + let promiseNotifications = PlacesTestUtils.waitForNotification( + "onItemChanged", + ( + id, + property, + isAnno, + newValue, + lastModified, + itemType, + parentId, + guid + ) => { + info(`Got a changed notification for ${guid}.`); + Assert.equal(property, "cleartime"); + Assert.ok(!isAnno); + Assert.equal(newValue, ""); + Assert.equal(lastModified, 0); + Assert.equal(itemType, PlacesUtils.bookmarks.TYPE_BOOKMARK); + guids.delete(guid); + return guids.size == 0; + } + ); + + await PlacesUtils.history.remove("http://book.ma.rk/"); + await promiseNotifications; +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bmindex.js b/toolkit/components/places/tests/bookmarks/test_bmindex.js new file mode 100644 index 0000000000..c79da88282 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bmindex.js @@ -0,0 +1,133 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- + * vim: sw=2 ts=2 et lcs=trail\:.,tab\:>~ : + * 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 NUM_BOOKMARKS = 20; +const NUM_SEPARATORS = 5; +const NUM_FOLDERS = 10; +const NUM_ITEMS = NUM_BOOKMARKS + NUM_SEPARATORS + NUM_FOLDERS; +const MIN_RAND = -5; +const MAX_RAND = 40; + +async function check_contiguous_indexes(bookmarks) { + var indexes = []; + for (let bm of bookmarks) { + let bmIndex = (await PlacesUtils.bookmarks.fetch(bm.guid)).index; + info(`Index: ${bmIndex}\n`); + info("Checking duplicates\n"); + Assert.ok(!indexes.includes(bmIndex)); + info(`Checking out of range, found ${bookmarks.length} items\n`); + Assert.ok(bmIndex >= 0 && bmIndex < bookmarks.length); + indexes.push(bmIndex); + } + info("Checking all valid indexes have been used\n"); + Assert.equal(indexes.length, bookmarks.length); +} + +add_task(async function test_bookmarks_indexing() { + let bookmarks = []; + // Insert bookmarks with random indexes. + for (let i = 0; bookmarks.length < NUM_BOOKMARKS; i++) { + let randIndex = Math.round( + MIN_RAND + Math.random() * (MAX_RAND - MIN_RAND) + ); + try { + let bm = await PlacesUtils.bookmarks.insert({ + index: randIndex, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: `Test bookmark ${i}`, + url: `http://${i}.mozilla.org/`, + }); + if (randIndex < -1) { + do_throw("Creating a bookmark at an invalid index should throw"); + } + bookmarks.push(bm); + } catch (ex) { + if (randIndex >= -1) { + do_throw("Creating a bookmark at a valid index should not throw"); + } + } + } + await check_contiguous_indexes(bookmarks); + + // Insert separators with random indexes. + for (let i = 0; bookmarks.length < NUM_BOOKMARKS + NUM_SEPARATORS; i++) { + let randIndex = Math.round( + MIN_RAND + Math.random() * (MAX_RAND - MIN_RAND) + ); + try { + let bm = await PlacesUtils.bookmarks.insert({ + index: randIndex, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + if (randIndex < -1) { + do_throw("Creating a separator at an invalid index should throw"); + } + bookmarks.push(bm); + } catch (ex) { + if (randIndex >= -1) { + do_throw("Creating a separator at a valid index should not throw"); + } + } + } + await check_contiguous_indexes(bookmarks); + + // Insert folders with random indexes. + for (let i = 0; bookmarks.length < NUM_ITEMS; i++) { + let randIndex = Math.round( + MIN_RAND + Math.random() * (MAX_RAND - MIN_RAND) + ); + try { + let bm = await PlacesUtils.bookmarks.insert({ + index: randIndex, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: `Test folder ${i}`, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + if (randIndex < -1) { + do_throw("Creating a folder at an invalid index should throw"); + } + bookmarks.push(bm); + } catch (ex) { + if (randIndex >= -1) { + do_throw("Creating a folder at a valid index should not throw"); + } + } + } + await check_contiguous_indexes(bookmarks); + + // Execute some random bookmark delete. + for (let i = 0; i < Math.ceil(NUM_ITEMS / 4); i++) { + let bm = bookmarks.splice( + Math.floor(Math.random() * bookmarks.length), + 1 + )[0]; + info(`Removing item with guid ${bm.guid}\n`); + await PlacesUtils.bookmarks.remove(bm); + } + await check_contiguous_indexes(bookmarks); + + // Execute some random bookmark move. This will also try to move it to + // invalid index values. + for (let i = 0; i < Math.ceil(NUM_ITEMS / 4); i++) { + let randIndex = Math.floor(Math.random() * bookmarks.length); + let bm = bookmarks[randIndex]; + let newIndex = Math.round(MIN_RAND + Math.random() * (MAX_RAND - MIN_RAND)); + info(`Moving item with guid ${bm.guid} to index ${newIndex}\n`); + try { + bm.index = newIndex; + await PlacesUtils.bookmarks.update(bm); + if (newIndex < -1) { + do_throw("Moving an item to a negative index should throw\n"); + } + } catch (ex) { + if (newIndex >= -1) { + do_throw("Moving an item to a valid index should not throw\n"); + } + } + } + await check_contiguous_indexes(bookmarks); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js new file mode 100644 index 0000000000..2ef2ec2cc2 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js @@ -0,0 +1,189 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test_eraseEverything() { + await PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://example.com/"), + }); + await PlacesTestUtils.addVisits({ + uri: NetUtil.newURI("http://mozilla.org/"), + }); + let frecencyForExample = frecencyForUrl("http://example.com/"); + let frecencyForMozilla = frecencyForUrl("http://example.com/"); + Assert.ok(frecencyForExample > 0); + Assert.ok(frecencyForMozilla > 0); + let unfiledFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + checkBookmarkObject(unfiledFolder); + let unfiledBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + }); + checkBookmarkObject(unfiledBookmark); + let unfiledBookmarkInFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: unfiledFolder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/", + }); + checkBookmarkObject(unfiledBookmarkInFolder); + + let menuFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + checkBookmarkObject(menuFolder); + let menuBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + }); + checkBookmarkObject(menuBookmark); + let menuBookmarkInFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: menuFolder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/", + }); + checkBookmarkObject(menuBookmarkInFolder); + + let toolbarFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + checkBookmarkObject(toolbarFolder); + let toolbarBookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + }); + checkBookmarkObject(toolbarBookmark); + let toolbarBookmarkInFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: toolbarFolder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://mozilla.org/", + }); + checkBookmarkObject(toolbarBookmarkInFolder); + + await PlacesTestUtils.promiseAsyncUpdates(); + Assert.ok(frecencyForUrl("http://example.com/") > frecencyForExample); + Assert.ok(frecencyForUrl("http://example.com/") > frecencyForMozilla); + + const promise = PlacesTestUtils.waitForNotification( + "pages-rank-changed", + () => true, + "places" + ); + + await PlacesUtils.bookmarks.eraseEverything(); + + // Ensure we get an pages-rank-changed event. + await promise; + + Assert.equal(frecencyForUrl("http://example.com/"), frecencyForExample); + Assert.equal(frecencyForUrl("http://example.com/"), frecencyForMozilla); +}); + +add_task(async function test_eraseEverything_roots() { + await PlacesUtils.bookmarks.eraseEverything(); + + // Ensure the roots have not been removed. + Assert.ok( + await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid) + ); + Assert.ok( + await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid) + ); + Assert.ok(await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.menuGuid)); + Assert.ok(await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.tagsGuid)); + Assert.ok(await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.rootGuid)); +}); + +add_task(async function test_eraseEverything_reparented() { + // Create a folder with 1 bookmark in it... + let folder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let bookmark1 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + url: "http://example.com/", + }); + // ...and a second folder. + let folder2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + // Reparent the bookmark to the 2nd folder. + bookmark1.parentGuid = folder2.guid; + await PlacesUtils.bookmarks.update(bookmark1); + + // Erase everything. + await PlacesUtils.bookmarks.eraseEverything(); + + // All the above items should no longer be in the GUIDHelper cache. + for (let guid of [folder1.guid, bookmark1.guid, folder2.guid]) { + await Assert.rejects( + PlacesUtils.promiseItemId(guid), + /no item found for the given GUID/ + ); + } +}); + +add_task(async function test_notifications() { + let bms = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "test", + url: "http://example.com", + }, + { + title: "folder", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "test2", + url: "http://example.com/2", + }, + ], + }, + ], + }); + + let skipDescendantsObserver = expectPlacesObserverNotifications( + ["bookmark-removed"], + false, + true + ); + let receiveAllObserver = expectPlacesObserverNotifications( + ["bookmark-removed"], + false + ); + + await PlacesUtils.bookmarks.eraseEverything(); + + let expectedNotifications = [ + { + type: "bookmark-removed", + guid: bms[1].guid, + }, + { + type: "bookmark-removed", + guid: bms[0].guid, + }, + ]; + + // If we're skipping descendents, we'll only be notified of the folder. + skipDescendantsObserver.check(expectedNotifications); + + // Note: Items of folders get notified first. + expectedNotifications.unshift({ + type: "bookmark-removed", + guid: bms[2].guid, + }); + + receiveAllObserver.check(expectedNotifications); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js new file mode 100644 index 0000000000..066b7b7e8e --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js @@ -0,0 +1,577 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var gAccumulator = { + get callback() { + this.results = []; + return result => this.results.push(result); + }, +}; + +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.fetch(), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch(null), + /Input should be a valid object/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guid: "123456789012", index: 0 }), + /The following properties were expected: parentGuid/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch({}), + /Unexpected number of conditions provided: 0/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.fetch({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + }), + /Unexpected number of conditions provided: 0/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.fetch({ + guid: "123456789012", + parentGuid: "012345678901", + index: 0, + }), + /Unexpected number of conditions provided: 2/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.fetch({ + guid: "123456789012", + url: "http://example.com", + }), + /Unexpected number of conditions provided: 2/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch("test"), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch(123), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guid: "test" }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guid: null }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guid: 123 }), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guidPrefix: "" }), + /Invalid value for property 'guidPrefix'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guidPrefix: null }), + /Invalid value for property 'guidPrefix'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guidPrefix: 123 }), + /Invalid value for property 'guidPrefix'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guidPrefix: "123456789012" }), + /Invalid value for property 'guidPrefix'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ guidPrefix: "@" }), + /Invalid value for property 'guidPrefix'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ parentGuid: "test", index: 0 }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ parentGuid: null, index: 0 }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ parentGuid: 123, index: 0 }), + /Invalid value for property 'parentGuid'/ + ); + + Assert.throws( + () => + PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", index: "0" }), + /Invalid value for property 'index'/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", index: null }), + /Invalid value for property 'index'/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.fetch({ parentGuid: "123456789012", index: -10 }), + /Invalid value for property 'index'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ url: "http://te st/" }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ url: null }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ url: -10 }), + /Invalid value for property 'url'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.fetch("123456789012", "test"), + /onResult callback must be a valid function/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch("123456789012", {}), + /onResult callback must be a valid function/ + ); +}); + +add_task(async function fetch_nonexistent_guid() { + let bm = await PlacesUtils.bookmarks.fetch( + { guid: "123456789012" }, + gAccumulator.callback + ); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(async function fetch_bookmark() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid, gAccumulator.callback); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://example.com/"); + Assert.equal(bm2.title, "a bookmark"); + + await PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(async function fetch_bookmar_empty_title() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.index, 0); + Assert.strictEqual(bm2.title, ""); + + await PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(async function fetch_folder() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.equal(bm2.title, "a folder"); + Assert.ok(!("url" in bm2)); + + await PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(async function fetch_folder_empty_title() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.index, 0); + Assert.strictEqual(bm2.title, ""); + + await PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(async function fetch_separator() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid); + checkBookmarkObject(bm2); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.ok(!("url" in bm2)); + Assert.strictEqual(bm2.title, ""); + + await PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(async function fetch_byguid_prefix() { + const PREFIX = "PREFIX-"; + + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + guid: PlacesUtils.generateGuidWithPrefix(PREFIX), + url: "http://bm1.example.com/", + title: "bookmark 1", + }); + checkBookmarkObject(bm1); + Assert.ok(bm1.guid.startsWith(PREFIX)); + + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + guid: PlacesUtils.generateGuidWithPrefix(PREFIX), + url: "http://bm2.example.com/", + title: "bookmark 2", + }); + checkBookmarkObject(bm2); + Assert.ok(bm2.guid.startsWith(PREFIX)); + + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + guid: PlacesUtils.generateGuidWithPrefix(PREFIX), + title: "a folder", + }); + checkBookmarkObject(bm3); + Assert.ok(bm3.guid.startsWith(PREFIX)); + + // Bookmark 4 doesn't have the same guid prefix, so it shouldn't be returned in the results. + let bm4 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://bm3.example.com/", + title: "bookmark 4", + }); + checkBookmarkObject(bm4); + Assert.ok(!bm4.guid.startsWith(PREFIX)); + + await PlacesUtils.bookmarks.fetch( + { guidPrefix: PREFIX }, + gAccumulator.callback + ); + + Assert.equal(gAccumulator.results.length, 3); + + // The results are returned by most recent first, so the first bookmark + // inserted is the last one in the returned array. + Assert.deepEqual(bm1, gAccumulator.results[2]); + Assert.deepEqual(bm2, gAccumulator.results[1]); + Assert.deepEqual(bm3, gAccumulator.results[0]); + + await PlacesUtils.bookmarks.remove(bm1); + await PlacesUtils.bookmarks.remove(bm2); + await PlacesUtils.bookmarks.remove(bm3); + await PlacesUtils.bookmarks.remove(bm4); +}); + +add_task(async function fetch_byposition_nonexisting_parentGuid() { + let bm = await PlacesUtils.bookmarks.fetch( + { parentGuid: "123456789012", index: 0 }, + gAccumulator.callback + ); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(async function fetch_byposition_nonexisting_index() { + let bm = await PlacesUtils.bookmarks.fetch( + { parentGuid: PlacesUtils.bookmarks.unfiledGuid, index: 100 }, + gAccumulator.callback + ); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(async function fetch_byposition() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch( + { parentGuid: bm1.parentGuid, index: bm1.index }, + gAccumulator.callback + ); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 0); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://example.com/"); + Assert.equal(bm2.title, "a bookmark"); +}); + +add_task(async function fetch_byposition_default_index() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/last", + title: "last child", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch( + { parentGuid: bm1.parentGuid, index: PlacesUtils.bookmarks.DEFAULT_INDEX }, + gAccumulator.callback + ); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.index, 1); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://example.com/last"); + Assert.equal(bm2.title, "last child"); + + await PlacesUtils.bookmarks.remove(bm1.guid); +}); + +add_task(async function fetch_byurl_nonexisting() { + let bm = await PlacesUtils.bookmarks.fetch( + { url: "http://nonexisting.com/" }, + gAccumulator.callback + ); + Assert.equal(bm, null); + Assert.equal(gAccumulator.results.length, 0); +}); + +add_task(async function fetch_byurl() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://byurl.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm1); + + // Also ensure that fecth-by-url excludes the tags folder. + PlacesUtils.tagging.tagURI(uri(bm1.url.href), ["Test Tag"]); + + let bm2 = await PlacesUtils.bookmarks.fetch( + { url: bm1.url }, + gAccumulator.callback + ); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + checkBookmarkObject(gAccumulator.results[0]); + Assert.deepEqual(gAccumulator.results[0], bm1); + + Assert.deepEqual(bm1, bm2); + Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.deepEqual(bm2.dateAdded, bm2.lastModified); + Assert.equal(bm2.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm2.url.href, "http://byurl.com/"); + Assert.equal(bm2.title, "a bookmark"); + + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://byurl.com/", + title: "a bookmark", + }); + let bm4 = await PlacesUtils.bookmarks.fetch( + { url: bm1.url }, + gAccumulator.callback + ); + checkBookmarkObject(bm4); + Assert.deepEqual(bm3, bm4); + Assert.equal(gAccumulator.results.length, 2); + gAccumulator.results.forEach(checkBookmarkObject); + Assert.deepEqual(gAccumulator.results[0], bm4); + + // After an update the returned bookmark should change. + await PlacesUtils.bookmarks.update({ guid: bm1.guid, title: "new title" }); + let bm5 = await PlacesUtils.bookmarks.fetch( + { url: bm1.url }, + gAccumulator.callback + ); + checkBookmarkObject(bm5); + // Cannot use deepEqual cause lastModified changed. + Assert.equal(bm1.guid, bm5.guid); + Assert.ok(bm5.lastModified > bm1.lastModified); + Assert.equal(gAccumulator.results.length, 2); + gAccumulator.results.forEach(checkBookmarkObject); + Assert.deepEqual(gAccumulator.results[0], bm5); + + // cleanup + PlacesUtils.tagging.untagURI(uri(bm1.url.href), ["Test Tag"]); +}); + +add_task(async function fetch_concurrent() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://concurrent.url.com/", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch( + { url: bm1.url }, + gAccumulator.callback, + { concurrent: true } + ); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + Assert.deepEqual(gAccumulator.results[0], bm1); + Assert.deepEqual(bm1, bm2); + let bm3 = await PlacesUtils.bookmarks.fetch( + { url: bm1.url }, + gAccumulator.callback, + { concurrent: false } + ); + checkBookmarkObject(bm3); + Assert.equal(gAccumulator.results.length, 1); + Assert.deepEqual(gAccumulator.results[0], bm1); + Assert.deepEqual(bm1, bm3); + let bm4 = await PlacesUtils.bookmarks.fetch( + { url: bm1.url }, + gAccumulator.callback, + {} + ); + checkBookmarkObject(bm4); + Assert.equal(gAccumulator.results.length, 1); + Assert.deepEqual(gAccumulator.results[0], bm1); + Assert.deepEqual(bm1, bm4); +}); + +add_task(async function fetch_by_parent() { + let folder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + checkBookmarkObject(folder1); + + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + url: "http://bm1.example.com/", + title: "bookmark 1", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + url: "http://bm2.example.com/", + title: "bookmark 2", + }); + checkBookmarkObject(bm2); + + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + url: "http://bm3.example.com/", + title: "bookmark 3", + index: 0, + }); + checkBookmarkObject(bm2); + + await PlacesUtils.bookmarks.fetch( + { parentGuid: folder1.guid }, + gAccumulator.callback + ); + + Assert.equal(gAccumulator.results.length, 3); + + Assert.equal(bm3.url.href, gAccumulator.results[0].url.href); + Assert.equal(bm1.url.href, gAccumulator.results[1].url.href); + Assert.equal(bm2.url.href, gAccumulator.results[2].url.href); + + await PlacesUtils.bookmarks.remove(folder1); +}); + +add_task(async function fetch_with_bookmark_path() { + let folder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "Parent", + }); + checkBookmarkObject(folder1); + + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + url: "http://bookmarkpath.example.com/", + title: "Child Bookmark", + }); + checkBookmarkObject(bm1); + + let bm2 = await PlacesUtils.bookmarks.fetch( + { guid: bm1.guid }, + gAccumulator.callback, + { includePath: true } + ); + checkBookmarkObject(bm2); + Assert.equal(gAccumulator.results.length, 1); + Assert.equal(bm2.path.length, 2); + Assert.equal(bm2.path[0].guid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm2.path[0].title, "unfiled"); + Assert.equal(bm2.path[1].guid, folder1.guid); + Assert.equal(bm2.path[1].title, folder1.title); + + await PlacesUtils.bookmarks.remove(folder1); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js new file mode 100644 index 0000000000..55cddb8820 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js @@ -0,0 +1,117 @@ +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.getRecent(), + /numberOfItems argument is required/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.getRecent("abc"), + /numberOfItems argument must be an integer/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.getRecent(1.2), + /numberOfItems argument must be an integer/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.getRecent(0), + /numberOfItems argument must be greater than zero/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.getRecent(-1), + /numberOfItems argument must be greater than zero/ + ); +}); + +add_task(async function getRecent_returns_recent_bookmarks() { + await PlacesUtils.bookmarks.eraseEverything(); + + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark", + }); + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "another bookmark", + }); + let bm4 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/path", + title: "yet another bookmark", + }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + checkBookmarkObject(bm4); + + // Add a tag to the most recent url to prove it doesn't get returned. + PlacesUtils.tagging.tagURI(uri(bm4.url), ["Test Tag"]); + + // Add a separator. + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + + // Add a query bookmark. + let queryURL = `place:parent=${PlacesUtils.bookmarks.menuGuid}&queryType=1`; + let bm5 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: queryURL, + title: "a test query", + }); + checkBookmarkObject(bm5); + + // Verify that getRecent only returns actual bookmarks. + let results = await PlacesUtils.bookmarks.getRecent(100); + Assert.equal( + results.length, + 4, + "The expected number of bookmarks was returned." + ); + checkBookmarkObject(results[0]); + Assert.deepEqual( + bm4, + results[0], + "The first result is the expected bookmark." + ); + checkBookmarkObject(results[1]); + Assert.deepEqual( + bm3, + results[1], + "The second result is the expected bookmark." + ); + checkBookmarkObject(results[2]); + Assert.deepEqual( + bm2, + results[2], + "The third result is the expected bookmark." + ); + checkBookmarkObject(results[3]); + Assert.deepEqual( + bm1, + results[3], + "The fourth result is the expected bookmark." + ); + + // Verify that getRecent utilizes the numberOfItems argument. + results = await PlacesUtils.bookmarks.getRecent(1); + Assert.equal( + results.length, + 1, + "The expected number of bookmarks was returned." + ); + checkBookmarkObject(results[0]); + Assert.deepEqual( + bm4, + results[0], + "The first result is the expected bookmark." + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js new file mode 100644 index 0000000000..e9959daf1a --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js @@ -0,0 +1,417 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.insert(), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert(null), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({}), + /The following properties were expected/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.insert({ guid: "test" }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ guid: null }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ guid: 123 }), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.insert({ parentGuid: "test" }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ parentGuid: null }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ parentGuid: 123 }), + /Invalid value for property 'parentGuid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.insert({ index: "1" }), + /Invalid value for property 'index'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ index: -10 }), + /Invalid value for property 'index'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.insert({ dateAdded: -10 }), + /Invalid value for property 'dateAdded'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ dateAdded: "today" }), + /Invalid value for property 'dateAdded'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ dateAdded: Date.now() }), + /Invalid value for property 'dateAdded'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.insert({ lastModified: -10 }), + /Invalid value for property 'lastModified'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ lastModified: "today" }), + /Invalid value for property 'lastModified'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ lastModified: Date.now() }), + /Invalid value for property 'lastModified'/ + ); + let time = new Date(); + + let past = new Date(time - 86400000); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ lastModified: past }), + /Invalid value for property 'lastModified'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.insert({ type: -1 }), + /Invalid value for property 'type'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ type: 100 }), + /Invalid value for property 'type'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.insert({ type: "bookmark" }), + /Invalid value for property 'type'/ + ); + + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + title: -1, + }), + /Invalid value for property 'title'/ + ); + + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: 10, + }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://te st", + }), + /Invalid value for property 'url'/ + ); + let longurl = "http://www.example.com/"; + for (let i = 0; i < 65536; i++) { + longurl += "a"; + } + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: longurl, + }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: NetUtil.newURI(longurl), + }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "te st", + }), + /Invalid value for property 'url'/ + ); +}); + +add_task(async function invalid_properties_for_bookmark_type() { + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + url: "http://www.moz.com/", + }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + url: "http://www.moz.com/", + }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + title: "test", + }), + /Invalid value for property 'title'/ + ); +}); + +add_task(async function test_insert_into_root_throws() { + const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm"); + const sandbox = sinon.createSandbox(); + sandbox.stub(PlacesUtils, "isInAutomation").get(() => false); + registerCleanupFunction(() => sandbox.restore()); + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.rootGuid, + url: "http://example.com", + }), + /Invalid value for property 'parentGuid'/, + "Should throw when inserting a bookmark into the root." + ); + Assert.throws( + () => + PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.rootGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }), + /Invalid value for property 'parentGuid'/, + "Should throw when inserting a folder into the root." + ); + sandbox.restore(); +}); + +add_task(async function long_title_trim() { + let longtitle = "a"; + for (let i = 0; i < 4096; i++) { + longtitle += "a"; + } + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: longtitle, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 0); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.equal(bm.title.length, 4096, "title should have been trimmed"); + Assert.ok(!("url" in bm), "url should not be set"); +}); + +add_task(async function create_separator() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 1); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.strictEqual(bm.title, ""); +}); + +add_task(async function create_separator_w_title_fail() { + try { + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + title: "a separator", + }); + Assert.ok(false, "Trying to set title for a separator should reject"); + } catch (ex) {} +}); + +add_task(async function create_separator_invalid_parent_fail() { + try { + await PlacesUtils.bookmarks.insert({ + parentGuid: "123456789012", + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + title: "a separator", + }); + Assert.ok( + false, + "Trying to create an item in a non existing parent reject" + ); + } catch (ex) {} +}); + +add_task(async function create_separator_given_guid() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + guid: "123456789012", + }); + checkBookmarkObject(bm); + Assert.equal(bm.guid, "123456789012"); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 2); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.strictEqual(bm.title, ""); +}); + +add_task(async function create_item_given_guid_no_type_fail() { + try { + await PlacesUtils.bookmarks.insert({ parentGuid: "123456789012" }); + Assert.ok( + false, + "Trying to create an item with a given guid but no type should reject" + ); + } catch (ex) {} +}); + +add_task(async function create_separator_big_index() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + index: 9999, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 3); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.strictEqual(bm.title, ""); +}); + +add_task(async function create_separator_given_dateAdded() { + let time = new Date(); + let past = new Date(time - 86400000); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + dateAdded: past, + }); + checkBookmarkObject(bm); + Assert.equal(bm.dateAdded, past); + Assert.equal(bm.lastModified, past); +}); + +add_task(async function create_folder() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.strictEqual(bm.title, ""); + + // And then create a nested folder. + let parentGuid = bm.guid; + bm = await PlacesUtils.bookmarks.insert({ + parentGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, parentGuid); + Assert.equal(bm.index, 0); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.strictEqual(bm.title, "a folder"); +}); + +add_task(async function create_bookmark() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let parentGuid = bm.guid; + + bm = await PlacesUtils.bookmarks.insert({ + parentGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, parentGuid); + Assert.equal(bm.index, 0); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm.url.href, "http://example.com/"); + Assert.equal(bm.title, "a bookmark"); + + // Check parent lastModified. + let parent = await PlacesUtils.bookmarks.fetch({ guid: bm.parentGuid }); + Assert.deepEqual(parent.lastModified, bm.dateAdded); + + bm = await PlacesUtils.bookmarks.insert({ + parentGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: new URL("http://example.com/"), + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, parentGuid); + Assert.equal(bm.index, 1); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm.url.href, "http://example.com/"); + Assert.strictEqual(bm.title, ""); +}); + +add_task(async function create_bookmark_frecency() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm); + + await PlacesTestUtils.promiseAsyncUpdates(); + Assert.greater(frecencyForUrl(bm.url), 0, "Check frecency has been updated"); +}); + +add_task(async function create_bookmark_without_type() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm.url.href, "http://example.com/"); + Assert.equal(bm.title, "a bookmark"); +}); + +add_task(async function test_url_with_apices() { + // Apices may confuse code and cause injection if mishandled. + const url = `javascript:alert("%s");alert('%s');`; + await PlacesUtils.bookmarks.insert({ + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + // Just a sanity check, this should not throw. + await PlacesUtils.history.remove(url); + let bm = await PlacesUtils.bookmarks.fetch({ url }); + await PlacesUtils.bookmarks.remove(bm); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js new file mode 100644 index 0000000000..825c23aef1 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js @@ -0,0 +1,581 @@ +add_task(async function invalid_input_rejects() { + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(), + /Should be provided a valid tree object./ + ); + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(null), + /Should be provided a valid tree object./ + ); + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree("foo"), + /Should be provided a valid tree object./ + ); + + // All subsequent tests pass a valid parent guid. + let guid = PlacesUtils.bookmarks.unfiledGuid; + + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree({ guid, children: [] }), + /Should have a non-zero number of children to insert./ + ); + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree({ guid }), + /Should have a non-zero number of children to insert./ + ); + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree({ children: [{}], guid }), + /The following properties were expected: url/ + ); + + // Reuse another variable to make this easier to read: + let tree = { guid, children: [{ guid: "test" }] }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'guid'/ + ); + tree.children = [{ guid: null }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'guid'/ + ); + tree.children = [{ guid: 123 }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'guid'/ + ); + + tree.children = [{ dateAdded: -10 }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'dateAdded'/ + ); + tree.children = [{ dateAdded: "today" }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'dateAdded'/ + ); + tree.children = [{ dateAdded: Date.now() }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'dateAdded'/ + ); + + tree.children = [{ lastModified: -10 }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'lastModified'/ + ); + tree.children = [{ lastModified: "today" }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'lastModified'/ + ); + tree.children = [{ lastModified: Date.now() }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'lastModified'/ + ); + + let time = new Date(); + let future = new Date(time + 86400000); + tree.children = [{ dateAdded: future, lastModified: time }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'dateAdded'/ + ); + let past = new Date(time - 86400000); + tree.children = [{ lastModified: past }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'lastModified'/ + ); + + tree.children = [{ type: -1 }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'type'/ + ); + tree.children = [{ type: 100 }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'type'/ + ); + tree.children = [{ type: "bookmark" }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'type'/ + ); + + tree.children = [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, title: -1 }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'title'/ + ); + + tree.children = [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: 10 }]; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(tree), + /Invalid value for property 'url'/ + ); + + let treeWithBrokenURL = { + children: [ + { type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "http://te st" }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(treeWithBrokenURL), + /Invalid value for property 'url'/ + ); + let longurl = "http://www.example.com/" + "a".repeat(65536); + let treeWithLongURL = { + children: [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: longurl }], + guid: PlacesUtils.bookmarks.unfiledGuid, + }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(treeWithLongURL), + /Invalid value for property 'url'/ + ); + let treeWithLongURI = { + children: [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: NetUtil.newURI(longurl), + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(treeWithLongURI), + /Invalid value for property 'url'/ + ); + let treeWithOtherBrokenURL = { + children: [{ type: PlacesUtils.bookmarks.TYPE_BOOKMARK, url: "te st" }], + guid: PlacesUtils.bookmarks.unfiledGuid, + }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(treeWithOtherBrokenURL), + /Invalid value for property 'url'/ + ); +}); + +add_task(async function invalid_properties_for_bookmark_type() { + let folderWithURL = { + children: [ + { type: PlacesUtils.bookmarks.TYPE_FOLDER, url: "http://www.moz.com/" }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(folderWithURL), + /Invalid value for property 'url'/ + ); + let separatorWithURL = { + children: [ + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + url: "http://www.moz.com/", + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(separatorWithURL), + /Invalid value for property 'url'/ + ); + let separatorWithTitle = { + children: [{ type: PlacesUtils.bookmarks.TYPE_SEPARATOR, title: "test" }], + guid: PlacesUtils.bookmarks.unfiledGuid, + }; + await Assert.throws( + () => PlacesUtils.bookmarks.insertTree(separatorWithTitle), + /Invalid value for property 'title'/ + ); +}); + +add_task(async function create_separator() { + let [bm] = await PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 0); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_SEPARATOR); + Assert.strictEqual(bm.title, ""); +}); + +add_task(async function create_plain_bm() { + let [bm] = await PlacesUtils.bookmarks.insertTree({ + children: [ + { + url: "http://www.example.com/", + title: "Test", + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 1); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_BOOKMARK); + Assert.equal(bm.title, "Test"); + Assert.equal(bm.url.href, "http://www.example.com/"); +}); + +add_task(async function create_folder() { + let [bm] = await PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "Test", + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + Assert.equal(bm.index, 2); + Assert.equal(bm.dateAdded, bm.lastModified); + Assert.equal(bm.type, PlacesUtils.bookmarks.TYPE_FOLDER); + Assert.equal(bm.title, "Test"); +}); + +add_task(async function create_in_tags() { + await Assert.throws( + () => + PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://www.example.com/", + title: "Test adding a tag", + }, + ], + guid: PlacesUtils.bookmarks.tagsGuid, + }), + /Can't use insertTree to insert tags/ + ); + let guidForTag = ( + await PlacesUtils.bookmarks.insert({ + title: "test-tag", + url: "http://www.unused.com/", + parentGuid: PlacesUtils.bookmarks.tagsGuid, + }) + ).guid; + await Assert.rejects( + PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://www.example.com/", + title: "Test adding an item to a tag", + }, + ], + guid: guidForTag, + }), + /Can't use insertTree to insert tags/ + ); + await PlacesUtils.bookmarks.remove(guidForTag); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function insert_into_root() { + await Assert.throws( + () => + PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://www.example.com/", + title: "Test inserting into root", + }, + ], + guid: PlacesUtils.bookmarks.rootGuid, + }), + /Can't insert into the root/ + ); +}); + +add_task(async function tree_where_separator_or_folder_has_kids() { + await Assert.throws( + () => + PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + children: [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://www.example.com/", + title: "Test inserting into separator", + }, + ], + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }), + /Invalid value for property 'children'/ + ); + + await Assert.throws( + () => + PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + children: [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://www.example.com/", + title: "Test inserting into separator", + }, + ], + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }), + /Invalid value for property 'children'/ + ); +}); + +add_task(async function create_hierarchy() { + let obsInvoked = 0; + let listener = events => { + for (let event of events) { + obsInvoked++; + Assert.greater(event.id, 0, "Should have a valid itemId"); + } + }; + PlacesUtils.observers.addListener(["bookmark-added"], listener); + let bms = await PlacesUtils.bookmarks.insertTree({ + children: [ + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "Root item", + children: [ + { + url: "http://www.example.com/1", + title: "BM 1", + }, + { + url: "http://www.example.com/2", + title: "BM 2", + }, + { + title: "Sub", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + children: [ + { + title: "Sub BM 1", + url: "http://www.example.com/sub/1", + }, + { + title: "Sub BM 2", + url: "http://www.example.com/sub/2", + }, + ], + }, + ], + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + PlacesUtils.observers.removeListener(["bookmark-added"], listener); + let parentFolder = null, + subFolder = null; + let prevBM = null; + for (let bm of bms) { + checkBookmarkObject(bm); + if (prevBM && prevBM.parentGuid == bm.parentGuid) { + Assert.equal(prevBM.index + 1, bm.index, "Indices should be subsequent"); + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm.guid)).index, + bm.index, + "Index reflects inserted index" + ); + } + prevBM = bm; + if (bm.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) { + Assert.greater( + frecencyForUrl(bm.url), + 0, + "Check frecency has been updated for bookmark " + bm.url + ); + } + if (bm.title == "Root item") { + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + parentFolder = bm; + } else if (!bm.title.startsWith("Sub BM")) { + Assert.equal(bm.parentGuid, parentFolder.guid); + if (bm.type == PlacesUtils.bookmarks.TYPE_FOLDER) { + subFolder = bm; + } + } else { + Assert.equal(bm.parentGuid, subFolder.guid); + } + } + Assert.equal(obsInvoked, bms.length); + Assert.equal(obsInvoked, 6); +}); + +add_task(async function insert_many_non_nested() { + let obsInvoked = 0; + let listener = events => { + for (let event of events) { + obsInvoked++; + Assert.greater(event.id, 0, "Should have a valid itemId"); + } + }; + PlacesUtils.observers.addListener(["bookmark-added"], listener); + let bms = await PlacesUtils.bookmarks.insertTree({ + children: [ + { + url: "http://www.example.com/1", + title: "Item 1", + }, + { + url: "http://www.example.com/2", + title: "Item 2", + }, + { + url: "http://www.example.com/3", + title: "Item 3", + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { + title: "Item 4", + url: "http://www.example.com/4", + }, + { + title: "Item 5", + url: "http://www.example.com/5", + }, + ], + guid: PlacesUtils.bookmarks.unfiledGuid, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + PlacesUtils.observers.removeListener(["bookmark-added"], listener); + let startIndex = -1; + for (let bm of bms) { + checkBookmarkObject(bm); + if (startIndex == -1) { + startIndex = bm.index; + } else { + Assert.equal(++startIndex, bm.index, "Indices should be subsequent"); + } + Assert.equal( + (await PlacesUtils.bookmarks.fetch(bm.guid)).index, + bm.index, + "Index reflects inserted index" + ); + if (bm.type == PlacesUtils.bookmarks.TYPE_BOOKMARK) { + Assert.greater( + frecencyForUrl(bm.url), + 0, + "Check frecency has been updated for bookmark " + bm.url + ); + } + Assert.equal(bm.parentGuid, PlacesUtils.bookmarks.unfiledGuid); + } + Assert.equal(obsInvoked, bms.length); + Assert.equal(obsInvoked, 6); +}); + +add_task(async function create_in_folder() { + let mozFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "Mozilla", + }); + + let notifications = []; + let listener = events => { + for (let event of events) { + notifications.push({ + itemId: event.id, + parentId: event.parentId, + index: event.index, + title: event.title, + guid: event.guid, + parentGuid: event.parentGuid, + }); + } + }; + PlacesUtils.observers.addListener(["bookmark-added"], listener); + + let bms = await PlacesUtils.bookmarks.insertTree({ + children: [ + { + url: "http://getfirefox.com", + title: "Get Firefox!", + }, + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "Community", + children: [ + { + url: "http://getthunderbird.com", + title: "Get Thunderbird!", + }, + { + url: "https://www.seamonkey-project.org", + title: "SeaMonkey", + }, + ], + }, + ], + guid: mozFolder.guid, + }); + await PlacesTestUtils.promiseAsyncUpdates(); + + PlacesUtils.observers.removeListener(["bookmark-added"], listener); + + let mozFolderId = await PlacesUtils.promiseItemId(mozFolder.guid); + let commFolderId = await PlacesUtils.promiseItemId(bms[1].guid); + deepEqual(notifications, [ + { + itemId: await PlacesUtils.promiseItemId(bms[0].guid), + parentId: mozFolderId, + index: 0, + title: "Get Firefox!", + guid: bms[0].guid, + parentGuid: mozFolder.guid, + }, + { + itemId: commFolderId, + parentId: mozFolderId, + index: 1, + title: "Community", + guid: bms[1].guid, + parentGuid: mozFolder.guid, + }, + { + itemId: await PlacesUtils.promiseItemId(bms[2].guid), + parentId: commFolderId, + index: 0, + title: "Get Thunderbird!", + guid: bms[2].guid, + parentGuid: bms[1].guid, + }, + { + itemId: await PlacesUtils.promiseItemId(bms[3].guid), + parentId: commFolderId, + index: 1, + title: "SeaMonkey", + guid: bms[3].guid, + parentGuid: bms[1].guid, + }, + ]); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_moveToFolder.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_moveToFolder.js new file mode 100644 index 0000000000..7cc3eb0916 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_moveToFolder.js @@ -0,0 +1,723 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function ensurePosition(info, parentGuid, index) { + print(`Checking ${info.guid}`); + checkBookmarkObject(info); + Assert.equal( + info.parentGuid, + parentGuid, + "Should be in the correct parent folder" + ); + Assert.equal(info.index, index, "Should have the correct index"); +} + +function insertChildren(folder, items) { + if (!items.length) { + return []; + } + + let children = []; + for (let i = 0; i < items.length; i++) { + if (items[i].type === TYPE_BOOKMARK) { + children.push({ + title: `${i}`, + url: "http://example.com", + }); + } else { + throw new Error(`Type ${items[i].type} is not supported.`); + } + } + return PlacesUtils.bookmarks.insertTree({ + guid: folder.guid, + children, + }); +} + +async function dumpFolderChildren( + folder, + details, + folderA, + folderB, + originalAChildren, + originalBChildren +) { + info(`${folder} Details:`); + info(`Input: ${JSON.stringify(details.initial[folder])}`); + info(`Expected: ${JSON.stringify(details.expected[folder])}`); + info("Index\tOriginal\tExpected\tResult"); + + let originalChildren; + let folderGuid; + if (folder == "folderA") { + originalChildren = originalAChildren; + folderGuid = folderA.guid; + } else { + originalChildren = originalBChildren; + folderGuid = folderB.guid; + } + + let tree = await PlacesUtils.promiseBookmarksTree(folderGuid); + let childrenCount = tree.children ? tree.children.length : 0; + for (let i = 0; i < originalChildren.length || i < childrenCount; i++) { + let originalGuid = + i < originalChildren.length ? originalChildren[i].guid : " "; + let resultGuid = i < childrenCount ? tree.children[i].guid : " "; + let expectedGuid = " "; + if (i < details.expected[folder].length) { + let expected = details.expected[folder][i]; + expectedGuid = + expected.folder == "a" + ? originalAChildren[expected.originalIndex].guid + : originalBChildren[expected.originalIndex].guid; + } + info(`${i}\t${originalGuid}\t${expectedGuid}\t${resultGuid}\n`); + } +} + +async function checkExpectedResults( + details, + folder, + folderGuid, + lastModified, + movedItems, + folderAChildren, + folderBChildren +) { + let expectedResults = details.expected[folder]; + for (let i = 0; i < expectedResults.length; i++) { + let expectedDetails = expectedResults[i]; + let originalItem = + expectedDetails.folder == "a" + ? folderAChildren[expectedDetails.originalIndex] + : folderBChildren[expectedDetails.originalIndex]; + + // Check the item got updated correctly in the database. + let updatedItem = await PlacesUtils.bookmarks.fetch(originalItem.guid); + + ensurePosition(updatedItem, folderGuid, i); + Assert.greaterOrEqual( + updatedItem.lastModified.getTime(), + lastModified.getTime(), + "Last modified should be later or equal to before" + ); + } + + if (details.expected.skipResultIndexChecks) { + return; + } + + // Check the items returned from the actual move() call are correct. + let index = 0; + for (let item of details.initial[folder]) { + if (!("targetFolder" in item)) { + // We weren't moving this item, so skip it and continue. + continue; + } + + let movedItem = movedItems[index]; + let updatedItem = await PlacesUtils.bookmarks.fetch(movedItem.guid); + + ensurePosition(movedItem, updatedItem.parentGuid, updatedItem.index); + + index++; + } +} + +async function checkLastModifiedForFolders(details, folder, movedItems) { + // For the tests, the moves always come from folderA. + if ( + details.initial.folderA.some( + item => "targetFolder" in item && item.targetFolder == folder.title + ) + ) { + let updatedFolder = await PlacesUtils.bookmarks.fetch(folder.guid); + + Assert.greaterOrEqual( + updatedFolder.lastModified.getTime(), + folder.lastModified.getTime(), + "Should have updated the folder's last modified time." + ); + print(JSON.stringify(movedItems[0])); + Assert.deepEqual( + updatedFolder.lastModified, + movedItems[0].lastModified, + "Should have the same last modified as the moved items." + ); + } +} + +async function testMoveToFolder(details) { + await PlacesUtils.bookmarks.eraseEverything(); + + // Always create two folders by default. + let [folderA, folderB] = await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.unfiledGuid, + children: [ + { + title: "a", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { + title: "b", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }, + ], + }); + + checkBookmarkObject(folderA); + let folderAChildren = await insertChildren(folderA, details.initial.folderA); + checkBookmarkObject(folderB); + let folderBChildren = await insertChildren(folderB, details.initial.folderB); + + const originalAChildren = folderAChildren.map(child => { + return { ...child }; + }); + const originalBChildren = folderBChildren.map(child => { + return { ...child }; + }); + + let lastModified; + if (folderAChildren.length) { + lastModified = folderAChildren[0].lastModified; + } else if (folderBChildren.length) { + lastModified = folderBChildren[0].lastModified; + } else { + throw new Error("No children added, can't determine lastModified"); + } + + // Work out which children to move and to where. + let childrenToUpdate = []; + for (let i = 0; i < details.initial.folderA.length; i++) { + if ("move" in details.initial.folderA[i]) { + childrenToUpdate.push(folderAChildren[i].guid); + } + } + + let observer; + if (details.notifications) { + observer = expectPlacesObserverNotifications(["bookmark-moved"]); + } + + let movedItems = await PlacesUtils.bookmarks.moveToFolder( + childrenToUpdate, + details.targetFolder == "a" ? folderA.guid : folderB.guid, + details.targetIndex + ); + + await dumpFolderChildren( + "folderA", + details, + folderA, + folderB, + originalAChildren, + originalBChildren + ); + await dumpFolderChildren( + "folderB", + details, + folderA, + folderB, + originalAChildren, + originalBChildren + ); + + Assert.equal(movedItems.length, childrenToUpdate.length); + await checkExpectedResults( + details, + "folderA", + folderA.guid, + lastModified, + movedItems, + folderAChildren, + folderBChildren + ); + await checkExpectedResults( + details, + "folderB", + folderB.guid, + lastModified, + movedItems, + folderAChildren, + folderBChildren + ); + + if (details.notifications) { + let expectedNotifications = []; + + for (let notification of details.notifications) { + let origItem = + notification.originalFolder == "folderA" + ? originalAChildren[notification.originalIndex] + : originalBChildren[notification.originalIndex]; + let newFolder = notification.newFolder == "folderA" ? folderA : folderB; + + expectedNotifications.push({ + type: "bookmark-moved", + id: await PlacesUtils.promiseItemId(origItem.guid), + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: origItem.url, + guid: origItem.guid, + parentGuid: newFolder.guid, + source: PlacesUtils.bookmarks.SOURCES.DEFAULT, + index: notification.newIndex, + oldParentGuid: origItem.parentGuid, + oldIndex: notification.originalIndex, + isTagging: false, + }); + } + observer.check(expectedNotifications); + } + + await checkLastModifiedForFolders(details, folderA, movedItems); + await checkLastModifiedForFolders(details, folderB, movedItems); +} + +const TYPE_BOOKMARK = PlacesUtils.bookmarks.TYPE_BOOKMARK; + +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.moveToFolder(), + /guids should be an array/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.moveToFolder({}), + /guids should be an array/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.moveToFolder([]), + /guids should be an array of at least one item/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.moveToFolder(["test"]), + /Expected only valid GUIDs to be passed/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.moveToFolder([null]), + /Expected only valid GUIDs to be passed/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.moveToFolder([123]), + /Expected only valid GUIDs to be passed/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.moveToFolder(["123456789012"], 123), + /Error: parentGuid should be a valid GUID/ + ); + + Assert.throws( + () => + PlacesUtils.bookmarks.moveToFolder( + ["123456789012"], + PlacesUtils.bookmarks.rootGuid + ), + /Cannot move bookmarks into root/ + ); + + Assert.throws( + () => + PlacesUtils.bookmarks.moveToFolder(["123456789012"], "123456789012", -2), + /index should be a number greater than/ + ); + Assert.throws( + () => + PlacesUtils.bookmarks.moveToFolder( + ["123456789012"], + "123456789012", + "sdffd" + ), + /index should be a number greater than/ + ); +}); + +add_task(async function test_move_nonexisting_bookmark_rejects() { + await Assert.rejects( + PlacesUtils.bookmarks.moveToFolder(["123456789012"], "123456789012", -1), + /No bookmarks found for the provided GUID/, + "Should reject when moving a non-existing bookmark" + ); +}); + +add_task(async function test_move_folder_into_descendant_rejects() { + let parent = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let descendant = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + await Assert.rejects( + PlacesUtils.bookmarks.moveToFolder([parent.guid], parent.guid, 0), + /Cannot insert a folder into itself or one of its descendants/, + "Should reject when moving a folder into itself" + ); + + await Assert.rejects( + PlacesUtils.bookmarks.moveToFolder([parent.guid], descendant.guid, 0), + /Cannot insert a folder into itself or one of its descendants/, + "Should reject when moving a folder into a descendant" + ); +}); + +add_task(async function test_move_from_differnt_with_no_target_rejects() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + await Assert.rejects( + PlacesUtils.bookmarks.moveToFolder([bm1.guid, bm2.guid], null, -1), + /All bookmarks should be in the same folder if no parent is specified/, + "Should reject when moving bookmarks from different folders with no target folder" + ); +}); + +add_task(async function test_move_append_same_folder() { + await testMoveToFolder({ + initial: { + folderA: [ + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK }, + ], + folderB: [], + }, + targetFolder: "a", + targetIndex: -1, + expected: { + folderA: [ + { folder: "a", originalIndex: 0 }, + { folder: "a", originalIndex: 2 }, + { folder: "a", originalIndex: 1 }, + ], + folderB: [], + }, + notifications: [ + { + originalFolder: "folderA", + originalIndex: 1, + newFolder: "folderA", + newIndex: 2, + }, + ], + }); +}); + +add_task(async function test_move_append_multiple_same_folder() { + await testMoveToFolder({ + initial: { + folderA: [ + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK }, + ], + folderB: [], + }, + targetFolder: "a", + targetIndex: -1, + expected: { + folderA: [ + { folder: "a", originalIndex: 3 }, + { folder: "a", originalIndex: 0 }, + { folder: "a", originalIndex: 1 }, + { folder: "a", originalIndex: 2 }, + ], + folderB: [], + }, + // These are all inserted at position 3 as that's what the views require + // to be notified, to ensure the new items are displayed in their correct + // positions. + notifications: [ + { + originalFolder: "folderA", + originalIndex: 0, + newFolder: "folderA", + newIndex: 3, + }, + { + originalFolder: "folderA", + originalIndex: 1, + newFolder: "folderA", + newIndex: 3, + }, + { + originalFolder: "folderA", + originalIndex: 2, + newFolder: "folderA", + newIndex: 3, + }, + ], + }); +}); + +add_task(async function test_move_append_multiple_new_folder() { + await testMoveToFolder({ + initial: { + folderA: [ + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK, move: true }, + ], + folderB: [], + }, + targetFolder: "b", + targetIndex: -1, + expected: { + folderA: [], + folderB: [ + { folder: "a", originalIndex: 0 }, + { folder: "a", originalIndex: 1 }, + { folder: "a", originalIndex: 2 }, + ], + }, + notifications: [ + { + originalFolder: "folderA", + originalIndex: 0, + newFolder: "folderB", + newIndex: 0, + }, + { + originalFolder: "folderA", + originalIndex: 1, + newFolder: "folderB", + newIndex: 1, + }, + { + originalFolder: "folderA", + originalIndex: 2, + newFolder: "folderB", + newIndex: 2, + }, + ], + }); +}); + +add_task(async function test_move_append_multiple_new_folder_with_existing() { + await testMoveToFolder({ + initial: { + folderA: [ + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK, move: true }, + ], + folderB: [{ type: TYPE_BOOKMARK }, { type: TYPE_BOOKMARK }], + }, + targetFolder: "b", + targetIndex: -1, + expected: { + folderA: [], + folderB: [ + { folder: "b", originalIndex: 0 }, + { folder: "b", originalIndex: 1 }, + { folder: "a", originalIndex: 0 }, + { folder: "a", originalIndex: 1 }, + { folder: "a", originalIndex: 2 }, + ], + }, + notifications: [ + { + originalFolder: "folderA", + originalIndex: 0, + newFolder: "folderB", + newIndex: 2, + }, + { + originalFolder: "folderA", + originalIndex: 1, + newFolder: "folderB", + newIndex: 3, + }, + { + originalFolder: "folderA", + originalIndex: 2, + newFolder: "folderB", + newIndex: 4, + }, + ], + }); +}); + +add_task(async function test_move_insert_same_folder_up() { + await testMoveToFolder({ + initial: { + folderA: [ + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK, move: true }, + ], + folderB: [], + }, + targetFolder: "a", + targetIndex: 0, + expected: { + folderA: [ + { folder: "a", originalIndex: 2 }, + { folder: "a", originalIndex: 0 }, + { folder: "a", originalIndex: 1 }, + ], + folderB: [], + }, + notifications: [ + { + originalFolder: "folderA", + originalIndex: 2, + newFolder: "folderA", + newIndex: 0, + }, + ], + }); +}); + +add_task(async function test_move_insert_same_folder_down() { + await testMoveToFolder({ + initial: { + folderA: [ + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK }, + ], + folderB: [], + }, + targetFolder: "a", + targetIndex: 2, + expected: { + folderA: [ + { folder: "a", originalIndex: 1 }, + { folder: "a", originalIndex: 0 }, + { folder: "a", originalIndex: 2 }, + ], + folderB: [], + }, + notifications: [ + { + originalFolder: "folderA", + originalIndex: 0, + newFolder: "folderA", + newIndex: 1, + }, + ], + }); +}); + +add_task( + async function test_move_insert_multiple_same_folder_split_locations() { + await testMoveToFolder({ + initial: { + folderA: [ + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK, move: true }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK }, + { type: TYPE_BOOKMARK, move: true }, + ], + folderB: [], + }, + targetFolder: "a", + targetIndex: 2, + expected: { + folderA: [ + { folder: "a", originalIndex: 1 }, + { folder: "a", originalIndex: 0 }, + { folder: "a", originalIndex: 3 }, + { folder: "a", originalIndex: 6 }, + { folder: "a", originalIndex: 9 }, + { folder: "a", originalIndex: 2 }, + { folder: "a", originalIndex: 4 }, + { folder: "a", originalIndex: 5 }, + { folder: "a", originalIndex: 7 }, + { folder: "a", originalIndex: 8 }, + ], + folderB: [], + }, + notifications: [ + { + originalFolder: "folderA", + originalIndex: 0, + newFolder: "folderA", + newIndex: 1, + }, + { + originalFolder: "folderA", + originalIndex: 3, + newFolder: "folderA", + newIndex: 2, + }, + { + originalFolder: "folderA", + originalIndex: 6, + newFolder: "folderA", + newIndex: 3, + }, + { + originalFolder: "folderA", + originalIndex: 9, + newFolder: "folderA", + newIndex: 4, + }, + ], + }); + } +); + +add_task(async function test_move_folder_with_descendant() { + let parent = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + }); + let descendant = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + Assert.equal(descendant.index, 1); + let lastModified = bm.lastModified; + + // This is moving to a nonexisting index by purpose, it will be appended. + [bm] = await PlacesUtils.bookmarks.moveToFolder( + [bm.guid], + descendant.guid, + 1 + ); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, descendant.guid); + Assert.equal(bm.index, 0); + Assert.ok(bm.lastModified >= lastModified); + + parent = await PlacesUtils.bookmarks.fetch(parent.guid); + descendant = await PlacesUtils.bookmarks.fetch(descendant.guid); + Assert.deepEqual(parent.lastModified, bm.lastModified); + Assert.deepEqual(descendant.lastModified, bm.lastModified); + Assert.equal(descendant.index, 0); + + bm = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal(bm.parentGuid, descendant.guid); + Assert.equal(bm.index, 0); + + [bm] = await PlacesUtils.bookmarks.moveToFolder([bm.guid], parent.guid, 0); + Assert.equal(bm.parentGuid, parent.guid); + Assert.equal(bm.index, 0); + + descendant = await PlacesUtils.bookmarks.fetch(descendant.guid); + Assert.equal(descendant.index, 1); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js new file mode 100644 index 0000000000..21b34838ed --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js @@ -0,0 +1,1130 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +add_task(async function insert_separator_notification() { + let observer = expectPlacesObserverNotifications(["bookmark-added"]); + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ + { + type: "bookmark-added", + id: itemId, + itemType: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentId, + index: bm.index, + url: bm.url, + title: bm.title, + dateAdded: bm.dateAdded, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function insert_folder_notification() { + let observer = expectPlacesObserverNotifications(["bookmark-added"]); + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "a folder", + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ + { + type: "bookmark-added", + id: itemId, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + parentId, + index: bm.index, + url: bm.url, + title: bm.title, + dateAdded: bm.dateAdded, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function insert_folder_notitle_notification() { + let observer = expectPlacesObserverNotifications(["bookmark-added"]); + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + strictEqual(bm.title, "", "Should return empty string for untitled folder"); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ + { + type: "bookmark-added", + id: itemId, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + parentId, + index: bm.index, + url: bm.url, + title: bm.title, + dateAdded: bm.dateAdded, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function insert_bookmark_notification() { + let observer = expectPlacesObserverNotifications(["bookmark-added"]); + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://example.com/"), + title: "a bookmark", + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ + { + type: "bookmark-added", + id: itemId, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentId, + index: bm.index, + url: bm.url, + title: bm.title, + dateAdded: bm.dateAdded, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function insert_bookmark_notitle_notification() { + let observer = expectPlacesObserverNotifications(["bookmark-added"]); + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://example.com/"), + }); + strictEqual(bm.title, "", "Should return empty string for untitled bookmark"); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + observer.check([ + { + type: "bookmark-added", + id: itemId, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentId, + index: bm.index, + url: bm.url, + title: bm.title, + dateAdded: bm.dateAdded, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function insert_bookmark_tag_notification() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://tag.example.com/"), + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + + let tagFolder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: "tag", + }); + const observer = expectPlacesObserverNotifications([ + "bookmark-added", + "bookmark-tags-changed", + ]); + let tag = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: tagFolder.guid, + url: new URL("http://tag.example.com/"), + }); + let tagId = await PlacesUtils.promiseItemId(tag.guid); + let tagParentId = await PlacesUtils.promiseItemId(tag.parentGuid); + + observer.check([ + { + type: "bookmark-added", + id: tagId, + parentId: tagParentId, + index: tag.index, + itemType: tag.type, + url: tag.url, + title: "", + dateAdded: tag.dateAdded, + guid: tag.guid, + parentGuid: tag.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: true, + }, + { + type: "bookmark-tags-changed", + id: itemId, + itemType: bm.type, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + tags: ["tag"], + lastModified: bm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function update_bookmark_lastModified() { + let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); + Preferences.set("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(function() { + Preferences.set("privacy.reduceTimerPrecision", timerPrecision); + }); + + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://lastmod.example.com/"), + }); + const observer = expectPlacesObserverNotifications(["bookmark-time-changed"]); + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + lastModified: new Date(), + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + + observer.check([ + { + type: "bookmark-time-changed", + id: itemId, + itemType: bm.type, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + dateAdded: bm.dateAdded, + lastModified: bm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function update_bookmark_title() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://title.example.com/"), + }); + const observer = expectPlacesObserverNotifications([ + "bookmark-title-changed", + ]); + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + title: "new title", + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + + observer.check([ + { + type: "bookmark-title-changed", + id: itemId, + itemType: bm.type, + url: bm.url, + title: bm.title, + guid: bm.guid, + parentGuid: bm.parentGuid, + lastModified: bm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function update_bookmark_uri() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://url.example.com/"), + }); + const observer = expectPlacesObserverNotifications(["bookmark-url-changed"]); + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + url: "http://mozilla.org/", + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + + observer.check([ + { + type: "bookmark-url-changed", + id: itemId, + itemType: bm.type, + url: bm.url.href, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + lastModified: bm.lastModified, + }, + ]); +}); + +add_task(async function update_move_same_folder() { + // Ensure there are at least two items in place (others test do so for us, + // but we don't have to depend on that). + await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://move.example.com/"), + }); + let bmItemId = await PlacesUtils.promiseItemId(bm.guid); + let bmOldIndex = bm.index; + + let observer = expectPlacesObserverNotifications(["bookmark-moved"]); + + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + Assert.equal(bm.index, 0); + observer.check([ + { + type: "bookmark-moved", + id: bmItemId, + itemType: bm.type, + url: "http://move.example.com/", + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + index: bm.index, + oldParentGuid: bm.parentGuid, + oldIndex: bmOldIndex, + isTagging: false, + }, + ]); + + // Test that we get the right index for DEFAULT_INDEX input. + bmOldIndex = 0; + observer = expectPlacesObserverNotifications(["bookmark-moved"]); + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + Assert.ok(bm.index > 0); + observer.check([ + { + type: "bookmark-moved", + id: bmItemId, + itemType: bm.type, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + index: bm.index, + oldParentGuid: bm.parentGuid, + oldIndex: bmOldIndex, + isTagging: false, + }, + ]); +}); + +add_task(async function update_move_different_folder() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://move.example.com/"), + }); + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let bmItemId = await PlacesUtils.promiseItemId(bm.guid); + let bmOldIndex = bm.index; + + const observer = expectPlacesObserverNotifications(["bookmark-moved"]); + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: folder.guid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + Assert.equal(bm.index, 0); + observer.check([ + { + type: "bookmark-moved", + id: bmItemId, + itemType: bm.type, + url: "http://move.example.com/", + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + index: bm.index, + oldParentGuid: PlacesUtils.bookmarks.unfiledGuid, + oldIndex: bmOldIndex, + isTagging: false, + }, + ]); +}); + +add_task(async function update_move_tag_folder() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://move.example.com/"), + }); + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: "tag", + }); + let bmItemId = await PlacesUtils.promiseItemId(bm.guid); + let bmOldIndex = bm.index; + + const observer = expectPlacesObserverNotifications(["bookmark-moved"]); + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: folder.guid, + index: PlacesUtils.bookmarks.DEFAULT_INDEX, + }); + Assert.equal(bm.index, 0); + observer.check([ + { + type: "bookmark-moved", + id: bmItemId, + itemType: bm.type, + url: "http://move.example.com/", + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + index: bm.index, + oldParentGuid: PlacesUtils.bookmarks.unfiledGuid, + oldIndex: bmOldIndex, + isTagging: true, + }, + ]); +}); + +add_task(async function remove_bookmark() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://remove.example.com/"), + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + + let observer = expectPlacesObserverNotifications(["bookmark-removed"]); + await PlacesUtils.bookmarks.remove(bm.guid); + observer.check([ + { + type: "bookmark-removed", + id: itemId, + parentId, + index: bm.index, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + ]); +}); + +add_task(async function remove_multiple_bookmarks() { + let bm1 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://remove.example.com/", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://remove1.example.com/", + }); + let itemId1 = await PlacesUtils.promiseItemId(bm1.guid); + let parentId1 = await PlacesUtils.promiseItemId(bm1.parentGuid); + let itemId2 = await PlacesUtils.promiseItemId(bm2.guid); + let parentId2 = await PlacesUtils.promiseItemId(bm2.parentGuid); + + let observer = expectPlacesObserverNotifications(["bookmark-removed"]); + await PlacesUtils.bookmarks.remove([bm1, bm2]); + observer.check([ + { + type: "bookmark-removed", + id: itemId1, + parentId: parentId1, + index: bm1.index, + url: bm1.url, + guid: bm1.guid, + parentGuid: bm1.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + { + type: "bookmark-removed", + id: itemId2, + parentId: parentId2, + index: bm2.index - 1, + url: bm2.url, + guid: bm2.guid, + parentGuid: bm2.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + ]); +}); + +add_task(async function remove_folder() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + + let observer = expectPlacesObserverNotifications(["bookmark-removed"]); + await PlacesUtils.bookmarks.remove(bm.guid); + observer.check([ + { + type: "bookmark-removed", + id: itemId, + parentId, + index: bm.index, + url: null, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: false, + }, + ]); +}); + +add_task(async function remove_bookmark_tag_notification() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://untag.example.com/"), + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + + let tagFolder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: "tag", + }); + let tag = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: tagFolder.guid, + url: new URL("http://untag.example.com/"), + }); + let tagId = await PlacesUtils.promiseItemId(tag.guid); + let tagParentId = await PlacesUtils.promiseItemId(tag.parentGuid); + + const observer = expectPlacesObserverNotifications([ + "bookmark-removed", + "bookmark-tags-changed", + ]); + await PlacesUtils.bookmarks.remove(tag.guid); + + observer.check([ + { + type: "bookmark-removed", + id: tagId, + parentId: tagParentId, + index: tag.index, + url: tag.url, + guid: tag.guid, + parentGuid: tag.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: true, + }, + { + type: "bookmark-tags-changed", + id: itemId, + itemType: bm.type, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + tags: [], + lastModified: bm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function rename_bookmark_tag_notification() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL("http://renametag.example.com/"), + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + + let tagFolder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: "tag", + }); + let tag = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: tagFolder.guid, + url: new URL("http://renametag.example.com/"), + }); + let tagParentId = await PlacesUtils.promiseItemId(tag.parentGuid); + + const observer = expectPlacesObserverNotifications([ + "bookmark-title-changed", + "bookmark-tags-changed", + ]); + tagFolder = await PlacesUtils.bookmarks.update({ + guid: tagFolder.guid, + title: "renamed", + }); + + observer.check([ + { + type: "bookmark-title-changed", + id: tagParentId, + title: "renamed", + guid: tagFolder.guid, + url: "", + lastModified: tagFolder.lastModified, + parentGuid: tagFolder.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: true, + }, + { + type: "bookmark-tags-changed", + id: itemId, + itemType: bm.type, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + tags: ["renamed"], + lastModified: bm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); + +add_task(async function remove_folder_notification() { + let folder1 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let folder1Id = await PlacesUtils.promiseItemId(folder1.guid); + let folder1ParentId = await PlacesUtils.promiseItemId(folder1.parentGuid); + + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder1.guid, + url: new URL("http://example.com/"), + }); + let bmItemId = await PlacesUtils.promiseItemId(bm.guid); + + let folder2 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: folder1.guid, + }); + let folder2Id = await PlacesUtils.promiseItemId(folder2.guid); + + let bm2 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder2.guid, + url: new URL("http://example.com/"), + }); + let bm2ItemId = await PlacesUtils.promiseItemId(bm2.guid); + + let observer = expectPlacesObserverNotifications(["bookmark-removed"]); + await PlacesUtils.bookmarks.remove(folder1.guid); + + observer.check([ + { + type: "bookmark-removed", + id: bm2ItemId, + parentId: folder2Id, + index: bm2.index, + url: bm2.url, + guid: bm2.guid, + parentGuid: bm2.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + { + type: "bookmark-removed", + id: folder2Id, + parentId: folder1Id, + index: folder2.index, + url: null, + guid: folder2.guid, + parentGuid: folder2.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: false, + }, + { + type: "bookmark-removed", + id: bmItemId, + parentId: folder1Id, + index: bm.index, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + { + type: "bookmark-removed", + id: folder1Id, + parentId: folder1ParentId, + index: folder1.index, + url: null, + guid: folder1.guid, + parentGuid: folder1.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: false, + }, + ]); +}); + +add_task(async function multiple_tags() { + const BOOKMARK_URL = "http://multipletags.example.com/"; + const TAG_NAMES = ["tag1", "tag2", "tag3", "tag4", "tag5", "tag6"]; + + const bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: new URL(BOOKMARK_URL), + }); + const itemId = await PlacesUtils.promiseItemId(bm.guid); + + info("Register all tags"); + const tagFolders = await Promise.all( + TAG_NAMES.map(tagName => + PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: tagName, + }) + ) + ); + + info("Test adding tags to bookmark"); + for (let i = 0; i < tagFolders.length; i++) { + const tagFolder = tagFolders[i]; + const expectedTagNames = TAG_NAMES.slice(0, i + 1); + + const observer = expectPlacesObserverNotifications([ + "bookmark-tags-changed", + ]); + + await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: tagFolder.guid, + url: new URL(BOOKMARK_URL), + }); + + observer.check([ + { + type: "bookmark-tags-changed", + id: itemId, + itemType: bm.type, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + tags: expectedTagNames, + lastModified: bm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); + } + + info("Test removing tags from bookmark"); + for (const removedLength of [1, 2, 3]) { + const removedTags = tagFolders.splice(0, removedLength); + + const observer = expectPlacesObserverNotifications([ + "bookmark-tags-changed", + ]); + + // We can remove multiple tags at one time. + await PlacesUtils.bookmarks.remove(removedTags); + + const expectedResults = []; + + for (let i = 0; i < removedLength; i++) { + TAG_NAMES.splice(0, 1); + const expectedTagNames = [...TAG_NAMES]; + + expectedResults.push({ + type: "bookmark-tags-changed", + id: itemId, + itemType: bm.type, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + tags: expectedTagNames, + lastModified: bm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }); + } + + observer.check(expectedResults); + } +}); + +add_task(async function eraseEverything_notification() { + // Let's start from a clean situation. + await PlacesUtils.bookmarks.eraseEverything(); + + let folder1 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let folder1Id = await PlacesUtils.promiseItemId(folder1.guid); + let folder1ParentId = await PlacesUtils.promiseItemId(folder1.parentGuid); + + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder1.guid, + url: new URL("http://example.com/"), + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + + let folder2 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let folder2Id = await PlacesUtils.promiseItemId(folder2.guid); + let folder2ParentId = await PlacesUtils.promiseItemId(folder2.parentGuid); + + let toolbarBm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: new URL("http://example.com/"), + }); + let toolbarBmId = await PlacesUtils.promiseItemId(toolbarBm.guid); + let toolbarBmParentId = await PlacesUtils.promiseItemId(toolbarBm.parentGuid); + + let menuBm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: new URL("http://example.com/"), + }); + let menuBmId = await PlacesUtils.promiseItemId(menuBm.guid); + let menuBmParentId = await PlacesUtils.promiseItemId(menuBm.parentGuid); + + let observer = expectPlacesObserverNotifications(["bookmark-removed"]); + await PlacesUtils.bookmarks.eraseEverything(); + + // Bookmarks should always be notified before their parents. + observer.check([ + { + type: "bookmark-removed", + id: itemId, + parentId, + index: bm.index, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + { + type: "bookmark-removed", + id: folder2Id, + parentId: folder2ParentId, + index: folder2.index, + url: null, + guid: folder2.guid, + parentGuid: folder2.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: false, + }, + { + type: "bookmark-removed", + id: folder1Id, + parentId: folder1ParentId, + index: folder1.index, + url: null, + guid: folder1.guid, + parentGuid: folder1.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: false, + }, + { + type: "bookmark-removed", + id: menuBmId, + parentId: menuBmParentId, + index: menuBm.index, + url: menuBm.url, + guid: menuBm.guid, + parentGuid: menuBm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + { + type: "bookmark-removed", + id: toolbarBmId, + parentId: toolbarBmParentId, + index: toolbarBm.index, + url: toolbarBm.url, + guid: toolbarBm.guid, + parentGuid: toolbarBm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + ]); +}); + +add_task(async function eraseEverything_reparented_notification() { + // Let's start from a clean situation. + await PlacesUtils.bookmarks.eraseEverything(); + + let folder1 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let folder1Id = await PlacesUtils.promiseItemId(folder1.guid); + let folder1ParentId = await PlacesUtils.promiseItemId(folder1.parentGuid); + + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: folder1.guid, + url: new URL("http://example.com/"), + }); + let itemId = await PlacesUtils.promiseItemId(bm.guid); + + let folder2 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let folder2Id = await PlacesUtils.promiseItemId(folder2.guid); + let folder2ParentId = await PlacesUtils.promiseItemId(folder2.parentGuid); + + bm.parentGuid = folder2.guid; + bm = await PlacesUtils.bookmarks.update(bm); + let parentId = await PlacesUtils.promiseItemId(bm.parentGuid); + + let observer = expectPlacesObserverNotifications(["bookmark-removed"]); + await PlacesUtils.bookmarks.eraseEverything(); + + // Bookmarks should always be notified before their parents. + observer.check([ + { + type: "bookmark-removed", + id: itemId, + parentId, + index: bm.index, + url: bm.url, + guid: bm.guid, + parentGuid: bm.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + isTagging: false, + }, + { + type: "bookmark-removed", + id: folder2Id, + parentId: folder2ParentId, + index: folder2.index, + url: null, + guid: folder2.guid, + parentGuid: folder2.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: false, + }, + { + type: "bookmark-removed", + id: folder1Id, + parentId: folder1ParentId, + index: folder1.index, + url: null, + guid: folder1.guid, + parentGuid: folder1.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + isTagging: false, + }, + ]); +}); + +add_task(async function reorder_notification() { + let bookmarks = [ + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example2.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example3.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + ]; + let sorted = []; + for (let bm of bookmarks) { + sorted.push(await PlacesUtils.bookmarks.insert(bm)); + } + + // Randomly reorder the array. + sorted.sort(() => 0.5 - Math.random()); + + const observer = expectPlacesObserverNotifications(["bookmark-moved"]); + await PlacesUtils.bookmarks.reorder( + PlacesUtils.bookmarks.unfiledGuid, + sorted.map(bm => bm.guid) + ); + + let expectedNotifications = []; + for (let i = 0; i < sorted.length; ++i) { + let child = sorted[i]; + let childId = await PlacesUtils.promiseItemId(child.guid); + expectedNotifications.push({ + type: "bookmark-moved", + id: childId, + itemType: child.type, + url: child.url || "", + guid: child.guid, + parentGuid: child.parentGuid, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + index: i, + oldParentGuid: child.parentGuid, + oldIndex: child.index, + isTagging: false, + }); + } + + observer.check(expectedNotifications); +}); + +add_task(async function update_notitle_notification() { + let toolbarBmURI = Services.io.newURI("https://example.com"); + let toolbarItemId = await PlacesUtils.promiseItemId( + PlacesUtils.bookmarks.toolbarGuid + ); + let toolbarBmId = PlacesUtils.bookmarks.insertBookmark( + toolbarItemId, + toolbarBmURI, + 0, + "Bookmark" + ); + let toolbarBmGuid = await PlacesUtils.promiseItemGuid(toolbarBmId); + + let menuFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + index: 0, + title: "Folder", + }); + let menuFolderId = await PlacesUtils.promiseItemId(menuFolder.guid); + + const observer = expectPlacesObserverNotifications([ + "bookmark-title-changed", + ]); + + PlacesUtils.bookmarks.setItemTitle(toolbarBmId, null); + strictEqual( + PlacesUtils.bookmarks.getItemTitle(toolbarBmId), + "", + "Legacy API should return empty string for untitled bookmark" + ); + + let updatedMenuBm = await PlacesUtils.bookmarks.update({ + guid: menuFolder.guid, + title: null, + }); + strictEqual( + updatedMenuBm.title, + "", + "Async API should return empty string for untitled bookmark" + ); + + let toolbarBmModified = await PlacesUtils.bookmarks.fetch(toolbarBmGuid); + observer.check([ + { + type: "bookmark-title-changed", + id: toolbarBmId, + itemType: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: toolbarBmURI.spec, + title: "", + guid: toolbarBmGuid, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + lastModified: toolbarBmModified.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + { + type: "bookmark-title-changed", + id: menuFolderId, + itemType: PlacesUtils.bookmarks.TYPE_FOLDER, + url: "", + title: "", + guid: menuFolder.guid, + parentGuid: PlacesUtils.bookmarks.menuGuid, + lastModified: updatedMenuBm.lastModified, + source: Ci.nsINavBookmarksService.SOURCE_DEFAULT, + isTagging: false, + }, + ]); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js new file mode 100644 index 0000000000..8e8fc55f24 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js @@ -0,0 +1,471 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +const UNVISITED_BOOKMARK_BONUS = 140; + +function promiseRankingChanged() { + return PlacesTestUtils.waitForNotification( + "pages-rank-changed", + () => true, + "places" + ); +} + +add_task(async function setup() { + Services.prefs.setIntPref( + "places.frecency.unvisitedBookmarkBonus", + UNVISITED_BOOKMARK_BONUS + ); +}); + +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.remove(), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove(null), + /Input should be a valid object/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.remove("test"), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove(123), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.remove({ guid: "test" }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove({ guid: null }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove({ guid: 123 }), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.remove({ parentGuid: "test" }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove({ parentGuid: null }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove({ parentGuid: 123 }), + /Invalid value for property 'parentGuid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.remove({ url: "http://te st/" }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove({ url: null }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.remove({ url: -10 }), + /Invalid value for property 'url'/ + ); +}); + +add_task(async function remove_nonexistent_guid() { + try { + await PlacesUtils.bookmarks.remove({ guid: "123456789012" }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/No bookmarks found for the provided GUID/.test(ex)); + } +}); + +add_task(async function remove_roots_fail() { + let guids = [ + PlacesUtils.bookmarks.rootGuid, + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.tagsGuid, + PlacesUtils.bookmarks.mobileGuid, + ]; + for (let guid of guids) { + Assert.throws( + () => PlacesUtils.bookmarks.remove(guid), + /It's not possible to remove Places root folders\./ + ); + } +}); + +add_task(async function remove_bookmark() { + // When removing a bookmark we need to check the frecency. First we confirm + // that there is a normal update when it is inserted. + let promise = promiseRankingChanged(); + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm1); + + await promise; + + // This second one checks the frecency is changed when we remove the bookmark. + promise = promiseRankingChanged(); + + await PlacesUtils.bookmarks.remove(bm1.guid); + + await promise; +}); + +add_task(async function remove_multiple_bookmarks_simple() { + // When removing a bookmark we need to check the frecency. First we confirm + // that there is a normal update when it is inserted. + const promise1 = promiseRankingChanged(); + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm1); + + const promise2 = promiseRankingChanged(); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example1.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm2); + + await Promise.all([promise1, promise2]); + + // We should get a pages-rank-changed event with the removal of + // multiple bookmarks. + const promise3 = promiseRankingChanged(); + + await PlacesUtils.bookmarks.remove([bm1, bm2]); + + await promise3; +}); + +add_task(async function remove_multiple_bookmarks_complex() { + let bms = []; + for (let i = 0; i < 10; i++) { + bms.push( + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: `http://example.com/${i}`, + title: `bookmark ${i}`, + }) + ); + } + + // Remove bookmarks 2 and 3. + let bmsToRemove = bms.slice(2, 4); + let notifiedIndexes = []; + let notificationPromise = PlacesTestUtils.waitForNotification( + "bookmark-removed", + events => { + for (let event of events) { + notifiedIndexes.push({ guid: event.guid, index: event.index }); + } + return notifiedIndexes.length == bmsToRemove.length; + }, + "places" + ); + await PlacesUtils.bookmarks.remove(bmsToRemove); + await notificationPromise; + + let indexModifier = 0; + for (let i = 0; i < bmsToRemove.length; i++) { + Assert.equal( + notifiedIndexes[i].guid, + bmsToRemove[i].guid, + `Should have been notified of the correct guid for item ${i}` + ); + Assert.equal( + notifiedIndexes[i].index, + bmsToRemove[i].index - indexModifier, + `Should have been notified of the correct index for the item ${i}` + ); + indexModifier++; + } + + let expectedIndex = 0; + for (let bm of [bms[0], bms[1], ...bms.slice(4)]) { + const fetched = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal( + fetched.index, + expectedIndex, + "Should have the correct index after consecutive item removal" + ); + bm.index = fetched.index; + expectedIndex++; + } + + // Remove some more including non-consecutive. + bmsToRemove = [bms[1], bms[5], bms[6], bms[8]]; + notifiedIndexes = []; + notificationPromise = PlacesTestUtils.waitForNotification( + "bookmark-removed", + events => { + for (let event of events) { + notifiedIndexes.push({ guid: event.guid, index: event.index }); + } + return notifiedIndexes.length == bmsToRemove.length; + }, + "places" + ); + await PlacesUtils.bookmarks.remove(bmsToRemove); + await notificationPromise; + + indexModifier = 0; + for (let i = 0; i < bmsToRemove.length; i++) { + Assert.equal( + notifiedIndexes[i].guid, + bmsToRemove[i].guid, + `Should have been notified of the correct guid for item ${i}` + ); + Assert.equal( + notifiedIndexes[i].index, + bmsToRemove[i].index - indexModifier, + `Should have been notified of the correct index for the item ${i}` + ); + indexModifier++; + } + + expectedIndex = 0; + const expectedRemaining = [bms[0], bms[4], bms[7], bms[9]]; + for (let bm of expectedRemaining) { + const fetched = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal( + fetched.index, + expectedIndex, + "Should have the correct index after non-consecutive item removal" + ); + expectedIndex++; + } + + // Tidy up + await PlacesUtils.bookmarks.remove(expectedRemaining); + await PlacesTestUtils.promiseAsyncUpdates(); +}); + +add_task(async function remove_bookmark_empty_title() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "", + }); + checkBookmarkObject(bm1); + + await PlacesUtils.bookmarks.remove(bm1.guid); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(bm1.guid), null); +}); + +add_task(async function remove_folder() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + checkBookmarkObject(bm1); + + await PlacesUtils.bookmarks.remove(bm1.guid); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(bm1.guid), null); + + // No wait for pages-rank-changed event in this test as the folder doesn't have + // any children that would need updating. +}); + +add_task(async function test_contents_removed() { + let folder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "", + }); + + let skipDescendantsObserver = expectPlacesObserverNotifications( + ["bookmark-removed"], + false, + true + ); + let receiveAllObserver = expectPlacesObserverNotifications( + ["bookmark-removed"], + false, + false + ); + const promise = promiseRankingChanged(); + await PlacesUtils.bookmarks.remove(folder1); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(folder1.guid), null); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(bm1.guid), null); + + await promise; + + let expectedNotifications = [ + { + type: "bookmark-removed", + guid: folder1.guid, + }, + ]; + + // If we're skipping descendents, we'll only be notified of the folder. + skipDescendantsObserver.check(expectedNotifications); + + // Note: Items of folders get notified first. + expectedNotifications.unshift({ + type: "bookmark-removed", + guid: bm1.guid, + }); + // If we don't skip descendents, we'll be notified of the folder and the + // bookmark. + receiveAllObserver.check(expectedNotifications); +}); + +add_task(async function test_nested_contents_removed() { + let folder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + let folder2 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder2.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "", + }); + + const promise = promiseRankingChanged(); + await PlacesUtils.bookmarks.remove(folder1); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(folder1.guid), null); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(folder2.guid), null); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(bm1.guid), null); + + await promise; +}); + +add_task(async function remove_folder_empty_title() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "", + }); + checkBookmarkObject(bm1); + + await PlacesUtils.bookmarks.remove(bm1.guid); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(bm1.guid), null); +}); + +add_task(async function remove_separator() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + checkBookmarkObject(bm1); + + await PlacesUtils.bookmarks.remove(bm1.guid); + Assert.strictEqual(await PlacesUtils.bookmarks.fetch(bm1.guid), null); +}); + +add_task(async function test_nested_content_fails_when_not_allowed() { + let folder1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: folder1.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a folder", + }); + await Assert.rejects( + PlacesUtils.bookmarks.remove(folder1, { + preventRemovalOfNonEmptyFolders: true, + }), + /Cannot remove a non-empty folder./ + ); +}); + +add_task(async function test_remove_bookmark_with_invalid_url() { + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title: "folder", + }); + let guid = "invalid_____"; + let folderedGuid = "invalid____2"; + let url = "invalid-uri"; + await PlacesUtils.withConnectionWrapper("test_bookmarks_remove", async db => { + await db.execute( + ` + INSERT INTO moz_places(url, url_hash, title, rev_host, guid) + VALUES (:url, hash(:url), 'Invalid URI', '.', GENERATE_GUID()) + `, + { url } + ); + await db.execute( + `INSERT INTO moz_bookmarks (type, fk, parent, position, guid) + VALUES (:type, + (SELECT id FROM moz_places WHERE url = :url), + (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid), + (SELECT MAX(position) + 1 FROM moz_bookmarks WHERE parent = (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid)), + :guid) + `, + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + guid, + } + ); + await db.execute( + `INSERT INTO moz_bookmarks (type, fk, parent, position, guid) + VALUES (:type, + (SELECT id FROM moz_places WHERE url = :url), + (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid), + (SELECT MAX(position) + 1 FROM moz_bookmarks WHERE parent = (SELECT id FROM moz_bookmarks WHERE guid = :parentGuid)), + :guid) + `, + { + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url, + parentGuid: folder.guid, + guid: folderedGuid, + } + ); + }); + await PlacesUtils.bookmarks.remove(guid); + Assert.strictEqual( + await PlacesUtils.bookmarks.fetch(guid), + null, + "Should not throw and not find the bookmark" + ); + + await PlacesUtils.bookmarks.remove(folder); + Assert.strictEqual( + await PlacesUtils.bookmarks.fetch(folderedGuid), + null, + "Should not throw and not find the bookmark" + ); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_remove_batch.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove_batch.js new file mode 100644 index 0000000000..9a3db7c1ce --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove_batch.js @@ -0,0 +1,134 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Test whether do batch removal if multiple bookmarks are removed at once. + +add_task(async function test_remove_multiple_bookmarks() { + info("Test for remove multiple bookmarks at once"); + + info("Insert multiple bookmarks"); + const testBookmarks = [ + { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/1", + }, + { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/2", + }, + { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/3", + }, + ]; + const bookmarks = await Promise.all( + testBookmarks.map(bookmark => PlacesUtils.bookmarks.insert(bookmark)) + ); + Assert.equal( + bookmarks.length, + testBookmarks.length, + "All test data are insterted correctly" + ); + + info("Remove multiple bookmarks"); + const onRemoved = PlacesTestUtils.waitForNotification( + "bookmark-removed", + () => true, + "places" + ); + await PlacesUtils.bookmarks.remove(bookmarks); + const events = await onRemoved; + + info("Check whether all bookmark-removed events called at at once or not"); + assertBookmarkRemovedEvents(events, bookmarks); +}); + +add_task(async function test_remove_folder_with_bookmarks() { + info("Test for remove a folder that has multiple bookmarks"); + + info("Insert a folder"); + const testFolder = { + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }; + const folder = await PlacesUtils.bookmarks.insert(testFolder); + Assert.ok(folder, "A folder is inserted correctly"); + + info("Insert multiple bookmarks to inserted folder"); + const testBookmarks = [ + { + parentGuid: folder.guid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/1", + }, + { + parentGuid: folder.guid, + url: "http://example.com/2", + }, + { + parentGuid: folder.guid, + url: "http://example.com/3", + }, + ]; + const bookmarks = await Promise.all( + testBookmarks.map(bookmark => PlacesUtils.bookmarks.insert(bookmark)) + ); + Assert.equal( + bookmarks.length, + testBookmarks.length, + "All test data are insterted correctly" + ); + + info("Remove the inserted folder"); + const notifiedEvents = []; + const onRemoved = PlacesTestUtils.waitForNotification( + "bookmark-removed", + events => { + notifiedEvents.push(events); + return notifiedEvents.length === 2; + }, + "places" + ); + await PlacesUtils.bookmarks.remove(folder); + await onRemoved; + + info("Check whether all bookmark-removed events called at at once or not"); + const eventsForBookmarks = notifiedEvents[0]; + assertBookmarkRemovedEvents(eventsForBookmarks, bookmarks); + + info("Check whether a bookmark-removed event called for the folder"); + const eventsForFolder = notifiedEvents[1]; + Assert.equal( + eventsForFolder.length, + 1, + "The length of notified events is correct" + ); + Assert.equal( + eventsForFolder[0].guid, + folder.guid, + "The guid of event is correct" + ); +}); + +function assertBookmarkRemovedEvents(events, expectedBookmarks) { + Assert.equal( + events.length, + expectedBookmarks.length, + "The length of notified events is correct" + ); + + for (let i = 0; i < events.length; i++) { + const event = events[i]; + const expectedBookmark = expectedBookmarks[i]; + Assert.equal( + event.guid, + expectedBookmark.guid, + `The guid of events[${i}] is correct` + ); + Assert.equal( + event.url, + expectedBookmark.url, + `The url of events[${i}] is correct` + ); + } +} diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js new file mode 100644 index 0000000000..7df909c704 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js @@ -0,0 +1,310 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.reorder(), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder(null), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.reorder("test"), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder(123), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.reorder({ guid: "test" }), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012"), + /Must provide a sorted array of children GUIDs./ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012", {}), + /Must provide a sorted array of children GUIDs./ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012", null), + /Must provide a sorted array of children GUIDs./ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012", []), + /Must provide a sorted array of children GUIDs./ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012", [null]), + /Invalid GUID found in the sorted children array/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012", [""]), + /Invalid GUID found in the sorted children array/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012", [{}]), + /Invalid GUID found in the sorted children array/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.reorder("123456789012", ["012345678901", null]), + /Invalid GUID found in the sorted children array/ + ); +}); + +add_task(async function reorder_nonexistent_guid() { + await Assert.rejects( + PlacesUtils.bookmarks.reorder("123456789012", ["012345678901"]), + /No folder found for the provided GUID/, + "Should throw for nonexisting guid" + ); +}); + +add_task(async function reorder() { + let bookmarks = [ + { + url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + url: "http://example2.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + { + url: "http://example3.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }, + ]; + + let sorted = []; + for (let bm of bookmarks) { + sorted.push(await PlacesUtils.bookmarks.insert(bm)); + } + + // Check the initial append sorting. + Assert.ok( + sorted.every((bm, i) => bm.index == i), + "Initial bookmarks sorting is correct" + ); + + // Apply random sorting and run multiple tests. + for (let t = 0; t < 4; t++) { + sorted.sort(() => 0.5 - Math.random()); + let sortedGuids = sorted.map(child => child.guid); + dump("Expected order: " + sortedGuids.join() + "\n"); + // Add a nonexisting guid to the array, to ensure nothing will break. + sortedGuids.push("123456789012"); + await PlacesUtils.bookmarks.reorder( + PlacesUtils.bookmarks.unfiledGuid, + sortedGuids + ); + for (let i = 0; i < sorted.length; ++i) { + let item = await PlacesUtils.bookmarks.fetch(sorted[i].guid); + Assert.equal(item.index, i); + } + } + + info("Test partial sorting"); + { + // Try a partial sorting by passing 2 entries in same order as they + // currently have. No entries should change order. + let sortedGuids = [sorted[0].guid, sorted[3].guid]; + dump("Expected order: " + sorted.map(b => b.guid).join() + "\n"); + await PlacesUtils.bookmarks.reorder( + PlacesUtils.bookmarks.unfiledGuid, + sortedGuids + ); + for (let i = 0; i < sorted.length; ++i) { + let item = await PlacesUtils.bookmarks.fetch(sorted[i].guid); + Assert.equal(item.index, i); + } + } + + { + // Try a partial sorting by passing 2 entries out of order + // The unspecified entries should be appended and retain the original order + sorted = [sorted[1], sorted[0]].concat(sorted.slice(2)); + let sortedGuids = [sorted[0].guid, sorted[1].guid]; + dump("Expected order: " + sorted.map(b => b.guid).join() + "\n"); + await PlacesUtils.bookmarks.reorder( + PlacesUtils.bookmarks.unfiledGuid, + sortedGuids + ); + for (let i = 0; i < sorted.length; ++i) { + let item = await PlacesUtils.bookmarks.fetch(sorted[i].guid); + Assert.equal(item.index, i); + } + } + // Use triangular numbers to detect skipped position. + let db = await PlacesUtils.promiseDBConnection(); + let rows = await db.execute( + `SELECT parent + FROM moz_bookmarks + GROUP BY parent + HAVING (SUM(DISTINCT position + 1) - (count(*) * (count(*) + 1) / 2)) <> 0` + ); + Assert.equal( + rows.length, + 0, + "All the bookmarks should have consistent positions" + ); +}); + +add_task(async function move_and_reorder() { + // Start clean. + await PlacesUtils.bookmarks.eraseEverything(); + + let bm1 = await PlacesUtils.bookmarks.insert({ + url: "http://example1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let f1 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + url: "http://example2.com/", + parentGuid: f1.guid, + }); + let f2 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + let bm3 = await PlacesUtils.bookmarks.insert({ + url: "http://example3.com/", + parentGuid: f2.guid, + }); + let bm4 = await PlacesUtils.bookmarks.insert({ + url: "http://example4.com/", + parentGuid: f2.guid, + }); + let bm5 = await PlacesUtils.bookmarks.insert({ + url: "http://example5.com/", + parentGuid: f2.guid, + }); + + // Invert f2 children. + // This is critical to reproduce the bug, cause it inverts the position + // compared to the natural insertion order. + await PlacesUtils.bookmarks.reorder(f2.guid, [bm5.guid, bm4.guid, bm3.guid]); + + bm1.parentGuid = f1.guid; + bm1.index = 0; + await PlacesUtils.bookmarks.update(bm1); + + bm1 = await PlacesUtils.bookmarks.fetch(bm1.guid); + Assert.equal(bm1.index, 0); + bm2 = await PlacesUtils.bookmarks.fetch(bm2.guid); + Assert.equal(bm2.index, 1); + bm3 = await PlacesUtils.bookmarks.fetch(bm3.guid); + Assert.equal(bm3.index, 2); + bm4 = await PlacesUtils.bookmarks.fetch(bm4.guid); + Assert.equal(bm4.index, 1); + bm5 = await PlacesUtils.bookmarks.fetch(bm5.guid); + Assert.equal(bm5.index, 0); + + // No-op reorder on f1 children. + // Nothing should change. Though, due to bug 1293365 this was causing children + // of other folders to get messed up. + await PlacesUtils.bookmarks.reorder(f1.guid, [bm1.guid, bm2.guid]); + + bm1 = await PlacesUtils.bookmarks.fetch(bm1.guid); + Assert.equal(bm1.index, 0); + bm2 = await PlacesUtils.bookmarks.fetch(bm2.guid); + Assert.equal(bm2.index, 1); + bm3 = await PlacesUtils.bookmarks.fetch(bm3.guid); + Assert.equal(bm3.index, 2); + bm4 = await PlacesUtils.bookmarks.fetch(bm4.guid); + Assert.equal(bm4.index, 1); + bm5 = await PlacesUtils.bookmarks.fetch(bm5.guid); + Assert.equal(bm5.index, 0); +}); + +add_task(async function reorder_empty_folder_invalid_children() { + // Start clean. + await PlacesUtils.bookmarks.eraseEverything(); + + let f1 = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + // Specifying a child that doesn't exist should cause that to be ignored. + // However, before bug 1333304, doing this on an empty folder threw. + await PlacesUtils.bookmarks.reorder(f1.guid, ["123456789012"]); +}); + +add_task(async function reorder_lastModified() { + // Start clean. + await PlacesUtils.bookmarks.eraseEverything(); + + let lastModified = new Date(Date.now() - 1000); + await PlacesUtils.bookmarks.insertTree({ + guid: PlacesUtils.bookmarks.menuGuid, + children: [ + { + guid: "bookmarkAAAA", + url: "http://example.com/a", + title: "A", + dateAdded: lastModified, + lastModified, + }, + { + guid: "bookmarkBBBB", + url: "http://example.com/b", + title: "B", + dateAdded: lastModified, + lastModified, + }, + ], + }); + await PlacesUtils.bookmarks.update({ + guid: PlacesUtils.bookmarks.menuGuid, + lastModified, + }); + + info("Reorder and set explicit last modified time"); + let newLastModified = new Date(lastModified.getTime() + 500); + await PlacesUtils.bookmarks.reorder( + PlacesUtils.bookmarks.menuGuid, + ["bookmarkBBBB", "bookmarkAAAA"], + { lastModified: newLastModified } + ); + for (let guid of [ + PlacesUtils.bookmarks.menuGuid, + "bookmarkAAAA", + "bookmarkBBBB", + ]) { + let info = await PlacesUtils.bookmarks.fetch(guid); + Assert.equal(info.lastModified.getTime(), newLastModified.getTime()); + } + + info("Reorder and set default last modified time"); + await PlacesUtils.bookmarks.reorder(PlacesUtils.bookmarks.menuGuid, [ + "bookmarkAAAA", + "bookmarkBBBB", + ]); + for (let guid of [ + PlacesUtils.bookmarks.menuGuid, + "bookmarkAAAA", + "bookmarkBBBB", + ]) { + let info = await PlacesUtils.bookmarks.fetch(guid); + Assert.greater(info.lastModified.getTime(), newLastModified.getTime()); + } +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js new file mode 100644 index 0000000000..92633a304a --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_search.js @@ -0,0 +1,339 @@ +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.search(), + /Query object is required/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.search(null), + /Query object is required/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.search({ title: 50 }), + /Title option must be a string/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.search({ url: { url: "wombat" } }), + /Url option must be a string or a URL object/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.search(50), + /Query must be an object or a string/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.search(true), + /Query must be an object or a string/ + ); +}); + +add_task(async function search_bookmark() { + await PlacesUtils.bookmarks.eraseEverything(); + + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/", + title: "another bookmark", + }); + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: "http://menu.org/", + title: "an on-menu bookmark", + }); + let bm4 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: "http://toolbar.org/", + title: "an on-toolbar bookmark", + }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + checkBookmarkObject(bm4); + + // finds a result by query + let results = await PlacesUtils.bookmarks.search("example.com"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // finds multiple results + results = await PlacesUtils.bookmarks.search("example"); + Assert.equal(results.length, 2); + checkBookmarkObject(results[0]); + checkBookmarkObject(results[1]); + + // finds menu bookmarks + results = await PlacesUtils.bookmarks.search("an on-menu bookmark"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm3, results[0]); + + // finds toolbar bookmarks + results = await PlacesUtils.bookmarks.search("an on-toolbar bookmark"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm4, results[0]); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function search_bookmark_by_query_object() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/", + title: "another bookmark", + }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + + let results = await PlacesUtils.bookmarks.search({ query: "example.com" }); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + + Assert.deepEqual(bm1, results[0]); + + results = await PlacesUtils.bookmarks.search({ query: "example" }); + Assert.equal(results.length, 2); + checkBookmarkObject(results[0]); + checkBookmarkObject(results[1]); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function search_bookmark_by_url() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark", + }); + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "third bookmark", + }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result by url + let results = await PlacesUtils.bookmarks.search({ + url: "http://example.com/", + }); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // normalizes the url + results = await PlacesUtils.bookmarks.search({ url: "http:/example.com" }); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // returns multiple matches + results = await PlacesUtils.bookmarks.search({ + url: "http://example.org/path", + }); + Assert.equal(results.length, 2); + + // requires exact match + results = await PlacesUtils.bookmarks.search({ url: "http://example.org/" }); + Assert.equal(results.length, 0); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function search_bookmark_by_title() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark", + }); + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "another bookmark", + }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result by title + let results = await PlacesUtils.bookmarks.search({ title: "a bookmark" }); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // returns multiple matches + results = await PlacesUtils.bookmarks.search({ title: "another bookmark" }); + Assert.equal(results.length, 2); + + // requires exact match + results = await PlacesUtils.bookmarks.search({ title: "bookmark" }); + Assert.equal(results.length, 0); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function search_bookmark_combinations() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.org/path", + title: "another bookmark", + }); + let bm3 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.net/", + title: "third bookmark", + }); + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + checkBookmarkObject(bm3); + + // finds the correct result if title and url match + let results = await PlacesUtils.bookmarks.search({ + url: "http://example.com/", + title: "a bookmark", + }); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm1, results[0]); + + // does not match if query is not matching but url and title match + results = await PlacesUtils.bookmarks.search({ + url: "http://example.com/", + title: "a bookmark", + query: "nonexistent", + }); + Assert.equal(results.length, 0); + + // does not match if one parameter is not matching + results = await PlacesUtils.bookmarks.search({ + url: "http://what.ever", + title: "a bookmark", + }); + Assert.equal(results.length, 0); + + // query only matches if other fields match as well + results = await PlacesUtils.bookmarks.search({ + query: "bookmark", + url: "http://example.net/", + }); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm3, results[0]); + + // non-matching query will also return no results + results = await PlacesUtils.bookmarks.search({ + query: "nonexistent", + url: "http://example.net/", + }); + Assert.equal(results.length, 0); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function search_folder() { + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "a test folder", + }); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(folder); + checkBookmarkObject(bm); + + // also finds folders + let results = await PlacesUtils.bookmarks.search("a test folder"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.equal(folder.title, results[0].title); + Assert.equal(folder.type, results[0].type); + Assert.equal(folder.parentGuid, results[0].parentGuid); + + // finds elements in folders + results = await PlacesUtils.bookmarks.search("example.com"); + Assert.equal(results.length, 1); + checkBookmarkObject(results[0]); + Assert.deepEqual(bm, results[0]); + Assert.equal(folder.guid, results[0].parentGuid); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function search_includes_separators() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + let bm2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + + checkBookmarkObject(bm1); + checkBookmarkObject(bm2); + + let results = await PlacesUtils.bookmarks.search({}); + Assert.ok( + results.findIndex(bookmark => { + return bookmark.guid == bm1.guid; + }) > -1, + "The bookmark was found in the results." + ); + Assert.ok( + results.findIndex(bookmark => { + return bookmark.guid == bm2.guid; + }) > -1, + "The separator was included in the results." + ); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function search_excludes_tags() { + let bm1 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + title: "a bookmark", + }); + checkBookmarkObject(bm1); + PlacesUtils.tagging.tagURI(uri(bm1.url.href), ["Test Tag"]); + + let results = await PlacesUtils.bookmarks.search("example.com"); + // If tags are not being excluded, this would return two results, one representing the tag. + Assert.equal(1, results.length, "A single object was returned from search."); + Assert.deepEqual(bm1, results[0], "The bookmark was returned."); + + results = await PlacesUtils.bookmarks.search("Test Tag"); + Assert.equal(0, results.length, "The tag folder was not returned."); + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js new file mode 100644 index 0000000000..e42c4b3f90 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js @@ -0,0 +1,577 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function invalid_input_throws() { + Assert.throws( + () => PlacesUtils.bookmarks.update(), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update(null), + /Input should be a valid object/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({}), + /The following properties were expected/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ guid: "test" }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ guid: null }), + /Invalid value for property 'guid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ guid: 123 }), + /Invalid value for property 'guid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ parentGuid: "test" }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ parentGuid: null }), + /Invalid value for property 'parentGuid'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ parentGuid: 123 }), + /Invalid value for property 'parentGuid'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ index: "1" }), + /Invalid value for property 'index'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ index: -10 }), + /Invalid value for property 'index'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ dateAdded: -10 }), + /Invalid value for property 'dateAdded'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ dateAdded: "today" }), + /Invalid value for property 'dateAdded'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ dateAdded: Date.now() }), + /Invalid value for property 'dateAdded'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ lastModified: -10 }), + /Invalid value for property 'lastModified'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ lastModified: "today" }), + /Invalid value for property 'lastModified'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ lastModified: Date.now() }), + /Invalid value for property 'lastModified'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ type: -1 }), + /Invalid value for property 'type'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ type: 100 }), + /Invalid value for property 'type'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ type: "bookmark" }), + /Invalid value for property 'type'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ url: 10 }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ url: "http://te st" }), + /Invalid value for property 'url'/ + ); + let longurl = "http://www.example.com/"; + for (let i = 0; i < 65536; i++) { + longurl += "a"; + } + Assert.throws( + () => PlacesUtils.bookmarks.update({ url: longurl }), + /Invalid value for property 'url'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ url: NetUtil.newURI(longurl) }), + /Invalid value for property 'url'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ url: "te st" }), + /Invalid value for property 'url'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ title: -1 }), + /Invalid value for property 'title'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.update({ title: {} }), + /Invalid value for property 'title'/ + ); + + Assert.throws( + () => PlacesUtils.bookmarks.update({ guid: "123456789012" }), + /Not enough properties to update/ + ); + + Assert.throws( + () => + PlacesUtils.bookmarks.update({ + guid: "123456789012", + parentGuid: "012345678901", + }), + /The following properties were expected: index/ + ); +}); + +add_task(async function move_roots_fail() { + let guids = [ + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.menuGuid, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.tagsGuid, + PlacesUtils.bookmarks.mobileGuid, + ]; + for (let guid of guids) { + await Assert.rejects( + PlacesUtils.bookmarks.update({ + guid, + index: -1, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }), + /It's not possible to move Places root folders\./, + `Should reject when attempting to move ${guid}` + ); + } +}); + +add_task(async function nonexisting_bookmark_throws() { + try { + await PlacesUtils.bookmarks.update({ guid: "123456789012", title: "test" }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/No bookmarks found for the provided GUID/.test(ex)); + } +}); + +add_task(async function invalid_properties_for_existing_bookmark() { + let bm = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + }); + + try { + await PlacesUtils.bookmarks.update({ + guid: bm.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/The bookmark type cannot be changed/.test(ex)); + } + + try { + await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: "123456789012", + index: 1, + }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/No bookmarks found for the provided parentGuid/.test(ex)); + } + + let past = new Date(Date.now() - 86400000); + try { + await PlacesUtils.bookmarks.update({ guid: bm.guid, lastModified: past }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'lastModified'/.test(ex)); + } + + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + try { + await PlacesUtils.bookmarks.update({ + guid: folder.guid, + url: "http://example.com/", + }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'url'/.test(ex)); + } + + let separator = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + try { + await PlacesUtils.bookmarks.update({ + guid: separator.guid, + url: "http://example.com/", + }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'url'/.test(ex)); + } + try { + await PlacesUtils.bookmarks.update({ guid: separator.guid, title: "test" }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok(/Invalid value for property 'title'/.test(ex)); + } +}); + +add_task(async function long_title_trim() { + let longtitle = "a"; + for (let i = 0; i < 4096; i++) { + longtitle += "a"; + } + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "title", + }); + checkBookmarkObject(bm); + + bm = await PlacesUtils.bookmarks.update({ guid: bm.guid, title: longtitle }); + let newTitle = bm.title; + Assert.equal(newTitle.length, 4096, "title should have been trimmed"); + + bm = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal(bm.title, newTitle); +}); + +add_task(async function update_lastModified() { + let yesterday = new Date(Date.now() - 86400000); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "title", + dateAdded: yesterday, + }); + checkBookmarkObject(bm); + Assert.deepEqual(bm.lastModified, yesterday); + + let time = new Date(); + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + lastModified: time, + }); + checkBookmarkObject(bm); + Assert.deepEqual(bm.lastModified, time); + + bm = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.deepEqual(bm.lastModified, time); + + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + lastModified: yesterday, + }); + Assert.deepEqual(bm.lastModified, yesterday); + + bm = await PlacesUtils.bookmarks.update({ guid: bm.guid, title: "title2" }); + Assert.ok(bm.lastModified >= time); + + bm = await PlacesUtils.bookmarks.update({ guid: bm.guid, title: "" }); + Assert.strictEqual(bm.title, ""); + + bm = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.strictEqual(bm.title, ""); +}); + +add_task(async function update_url() { + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + url: "http://example.com/", + title: "title", + }); + checkBookmarkObject(bm); + let lastModified = bm.lastModified; + let frecency = frecencyForUrl(bm.url); + Assert.greater(frecency, 0, "Check frecency has been updated"); + + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + url: "http://mozilla.org/", + }); + checkBookmarkObject(bm); + Assert.ok(bm.lastModified >= lastModified); + Assert.equal(bm.url.href, "http://mozilla.org/"); + + bm = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal(bm.url.href, "http://mozilla.org/"); + Assert.ok(bm.lastModified >= lastModified); + + Assert.equal( + frecencyForUrl("http://example.com/"), + frecency, + "Check frecency for example.com" + ); + Assert.equal( + frecencyForUrl("http://mozilla.org/"), + frecency, + "Check frecency for mozilla.org" + ); +}); + +add_task(async function update_index() { + let parent = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let f1 = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + Assert.equal(f1.index, 0); + let f2 = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + Assert.equal(f2.index, 1); + let f3 = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + Assert.equal(f3.index, 2); + let lastModified = f1.lastModified; + + f1 = await PlacesUtils.bookmarks.update({ + guid: f1.guid, + parentGuid: f1.parentGuid, + index: 1, + }); + checkBookmarkObject(f1); + Assert.equal(f1.index, 1); + Assert.ok(f1.lastModified >= lastModified); + + parent = await PlacesUtils.bookmarks.fetch(f1.parentGuid); + Assert.deepEqual(parent.lastModified, f1.lastModified); + + f2 = await PlacesUtils.bookmarks.fetch(f2.guid); + Assert.equal(f2.index, 0); + + f3 = await PlacesUtils.bookmarks.fetch(f3.guid); + Assert.equal(f3.index, 2); + + f3 = await PlacesUtils.bookmarks.update({ guid: f3.guid, index: 0 }); + f1 = await PlacesUtils.bookmarks.fetch(f1.guid); + Assert.equal(f1.index, 2); + + f2 = await PlacesUtils.bookmarks.fetch(f2.guid); + Assert.equal(f2.index, 1); +}); + +add_task(async function update_move_folder_into_descendant_throws() { + let parent = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let descendant = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + try { + await PlacesUtils.bookmarks.update({ + guid: parent.guid, + parentGuid: parent.guid, + index: 0, + }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok( + /Cannot insert a folder into itself or one of its descendants/.test(ex) + ); + } + + try { + await PlacesUtils.bookmarks.update({ + guid: parent.guid, + parentGuid: descendant.guid, + index: 0, + }); + Assert.ok(false, "Should have thrown"); + } catch (ex) { + Assert.ok( + /Cannot insert a folder into itself or one of its descendants/.test(ex) + ); + } +}); + +add_task(async function update_move_into_root_folder_rejects() { + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + }); + + Assert.throws( + () => + PlacesUtils.bookmarks.update({ + guid: bm.guid, + index: -1, + parentGuid: PlacesUtils.bookmarks.rootGuid, + }), + /Invalid value for property 'parentGuid'/, + "Should reject when attempting to move a bookmark into the root" + ); + + Assert.throws( + () => + PlacesUtils.bookmarks.update({ + guid: folder.guid, + index: -1, + parentGuid: PlacesUtils.bookmarks.rootGuid, + }), + /Invalid value for property 'parentGuid'/, + "Should reject when attempting to move a bookmark into the root" + ); +}); + +add_task(async function update_move() { + let parent = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + let bm = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + url: "http://example.com/", + type: PlacesUtils.bookmarks.TYPE_BOOKMARK, + }); + let descendant = await PlacesUtils.bookmarks.insert({ + parentGuid: parent.guid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + Assert.equal(descendant.index, 1); + let lastModified = bm.lastModified; + + // This is moving to a nonexisting index by purpose, it will be appended. + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: descendant.guid, + index: 1, + }); + checkBookmarkObject(bm); + Assert.equal(bm.parentGuid, descendant.guid); + Assert.equal(bm.index, 0); + Assert.ok(bm.lastModified >= lastModified); + + parent = await PlacesUtils.bookmarks.fetch(parent.guid); + descendant = await PlacesUtils.bookmarks.fetch(descendant.guid); + Assert.deepEqual(parent.lastModified, bm.lastModified); + Assert.deepEqual(descendant.lastModified, bm.lastModified); + Assert.equal(descendant.index, 0); + + bm = await PlacesUtils.bookmarks.fetch(bm.guid); + Assert.equal(bm.parentGuid, descendant.guid); + Assert.equal(bm.index, 0); + + bm = await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: parent.guid, + index: 0, + }); + Assert.equal(bm.parentGuid, parent.guid); + Assert.equal(bm.index, 0); + + descendant = await PlacesUtils.bookmarks.fetch(descendant.guid); + Assert.equal(descendant.index, 1); +}); + +add_task(async function update_move_append() { + let folder_a = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + checkBookmarkObject(folder_a); + let folder_b = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + checkBookmarkObject(folder_b); + + /* folder_a: [sep_1, sep_2, sep_3], folder_b: [] */ + let sep_1 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder_a.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + checkBookmarkObject(sep_1); + let sep_2 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder_a.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + checkBookmarkObject(sep_2); + let sep_3 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder_a.guid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + checkBookmarkObject(sep_3); + + function ensurePosition(info, parentGuid, index) { + checkBookmarkObject(info); + Assert.equal(info.parentGuid, parentGuid); + Assert.equal(info.index, index); + } + + // folder_a: [sep_2, sep_3, sep_1], folder_b: [] + sep_1.index = PlacesUtils.bookmarks.DEFAULT_INDEX; + // Note sep_1 includes parentGuid even though we're not moving the item to + // another folder + sep_1 = await PlacesUtils.bookmarks.update(sep_1); + ensurePosition(sep_1, folder_a.guid, 2); + sep_2 = await PlacesUtils.bookmarks.fetch(sep_2.guid); + ensurePosition(sep_2, folder_a.guid, 0); + sep_3 = await PlacesUtils.bookmarks.fetch(sep_3.guid); + ensurePosition(sep_3, folder_a.guid, 1); + sep_1 = await PlacesUtils.bookmarks.fetch(sep_1.guid); + ensurePosition(sep_1, folder_a.guid, 2); + + // folder_a: [sep_2, sep_1], folder_b: [sep_3] + sep_3.index = PlacesUtils.bookmarks.DEFAULT_INDEX; + sep_3.parentGuid = folder_b.guid; + sep_3 = await PlacesUtils.bookmarks.update(sep_3); + ensurePosition(sep_3, folder_b.guid, 0); + sep_2 = await PlacesUtils.bookmarks.fetch(sep_2.guid); + ensurePosition(sep_2, folder_a.guid, 0); + sep_1 = await PlacesUtils.bookmarks.fetch(sep_1.guid); + ensurePosition(sep_1, folder_a.guid, 1); + sep_3 = await PlacesUtils.bookmarks.fetch(sep_3.guid); + ensurePosition(sep_3, folder_b.guid, 0); + + // folder_a: [sep_1], folder_b: [sep_3, sep_2] + sep_2.index = Number.MAX_SAFE_INTEGER; + sep_2.parentGuid = folder_b.guid; + sep_2 = await PlacesUtils.bookmarks.update(sep_2); + ensurePosition(sep_2, folder_b.guid, 1); + sep_1 = await PlacesUtils.bookmarks.fetch(sep_1.guid); + ensurePosition(sep_1, folder_a.guid, 0); + sep_3 = await PlacesUtils.bookmarks.fetch(sep_3.guid); + ensurePosition(sep_3, folder_b.guid, 0); + sep_2 = await PlacesUtils.bookmarks.fetch(sep_2.guid); + ensurePosition(sep_2, folder_b.guid, 1); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js b/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js new file mode 100644 index 0000000000..be6b4ad669 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js @@ -0,0 +1,23 @@ +// Bug 1192692 - promiseBookmarksTree caches items without adding observers to +// invalidate the cache. +add_task(async function boookmarks_tree_cache() { + // Note that for this test to be effective, it needs to use the "old" sync + // bookmarks methods - using, eg, PlacesUtils.bookmarks.insert() doesn't + // demonstrate the problem as it indirectly arranges for the observers to + // be added. + let id = PlacesUtils.bookmarks.insertBookmark( + await PlacesUtils.promiseItemId(PlacesUtils.bookmarks.unfiledGuid), + uri("http://example.com"), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "A title" + ); + + await PlacesUtils.promiseBookmarksTree(); + + PlacesUtils.bookmarks.removeItem(id); + + await Assert.rejects( + PlacesUtils.promiseItemGuid(id), + /no item found for the given itemId/ + ); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_insertTree_fixupOrSkipInvalidEntries.js b/toolkit/components/places/tests/bookmarks/test_insertTree_fixupOrSkipInvalidEntries.js new file mode 100644 index 0000000000..bd71ce59e0 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_insertTree_fixupOrSkipInvalidEntries.js @@ -0,0 +1,114 @@ +function insertTree(tree) { + return PlacesUtils.bookmarks.insertTree(tree, { + fixupOrSkipInvalidEntries: true, + }); +} + +add_task(async function() { + let guid = PlacesUtils.bookmarks.unfiledGuid; + await Assert.throws( + () => insertTree({ guid, children: [] }), + /Should have a non-zero number of children to insert./ + ); + await Assert.throws( + () => insertTree({ guid: "invalid", children: [{}] }), + /The parent guid is not valid/ + ); + + let now = new Date(); + let url = "http://mozilla.com/"; + let obs = { + count: 0, + lastIndex: 0, + handlePlacesEvent(events) { + for (let event of events) { + obs.count++; + let lastIndex = obs.lastIndex; + obs.lastIndex = event.index; + if (event.itemType == PlacesUtils.bookmarks.TYPE_BOOKMARK) { + Assert.equal(event.url, url, "Found the expected url"); + } + Assert.ok( + event.index == 0 || event.index == lastIndex + 1, + "Consecutive indices" + ); + Assert.ok(event.dateAdded >= now, "Found a valid dateAdded"); + Assert.ok(PlacesUtils.isValidGuid(event.guid), "guid is valid"); + } + }, + }; + PlacesUtils.observers.addListener(["bookmark-added"], obs.handlePlacesEvent); + + let tree = { + guid, + children: [ + { + // Should be inserted, and the invalid guid should be replaced. + guid: "test", + url, + }, + { + // Should be skipped, since the url is invalid. + url: "fake_url", + }, + { + // Should be skipped, since the type is invalid. + url, + type: 999, + }, + { + // Should be skipped, since the type is invalid. + type: 999, + children: [ + { + url, + }, + ], + }, + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + title: "test", + children: [ + { + // Should fix lastModified and dateAdded. + url, + lastModified: null, + }, + { + // Should be skipped, since the url is invalid. + url: "fake_url", + dateAdded: null, + }, + { + // Should fix lastModified and dateAdded. + url, + dateAdded: undefined, + }, + { + // Should be skipped since it's a separator with a url + url, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { + // Should fix lastModified and dateAdded. + url, + dateAdded: new Date(now - 86400000), + lastModified: new Date(now - 172800000), // less than dateAdded + }, + ], + }, + ], + }; + + let bms = await insertTree(tree); + for (let bm of bms) { + checkBookmarkObject(bm); + } + Assert.equal(bms.length, 5); + Assert.equal(obs.count, bms.length); + + PlacesUtils.observers.removeListener( + ["bookmark-added"], + obs.handlePlacesEvent + ); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_keywords.js b/toolkit/components/places/tests/bookmarks/test_keywords.js new file mode 100644 index 0000000000..9b5be8f595 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_keywords.js @@ -0,0 +1,767 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +ChromeUtils.defineESModuleGetters(this, { + Preferences: "resource://gre/modules/Preferences.sys.mjs", +}); + +const URI1 = "http://test1.mozilla.org/"; +const URI2 = "http://test2.mozilla.org/"; +const URI3 = "http://test3.mozilla.org/"; + +async function check_keyword(aURI, aKeyword) { + if (aKeyword) { + aKeyword = aKeyword.toLowerCase(); + } + + if (aKeyword) { + let uri = await PlacesUtils.keywords.fetch(aKeyword); + Assert.equal(uri.url, aURI); + // Check case insensitivity. + uri = await PlacesUtils.keywords.fetch(aKeyword.toUpperCase()); + Assert.equal(uri.url, aURI); + } else { + let entry = await PlacesUtils.keywords.fetch({ url: aURI }); + if (entry) { + throw new Error(`${aURI.spec} should not have a keyword`); + } + } +} + +async function check_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 expectNotifications() { + 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( + id, + prop, + isAnno, + val, + lastMod, + itemType, + parentId, + guid, + parentGuid, + oldVal + ) { + if (prop != "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, arguments: args }); + }; + } + + return target[name]; + }, + }); + PlacesUtils.bookmarks.addObserver(observer); + return observer; +} + +add_task(function test_invalid_input() {}); + +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(URI1, null); + let fc = await foreign_count(URI1); + let observer = expectNotifications(); + + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: URI1, + title: "test", + }); + await PlacesUtils.keywords.insert({ url: URI1, keyword: "keyword" }); + let itemId = await PlacesUtils.promiseItemId(bookmark.guid); + observer.check([ + { + name: "onItemChanged", + arguments: [ + itemId, + "keyword", + false, + "keyword", + bookmark.lastModified * 1000, + bookmark.type, + await PlacesUtils.promiseItemId(bookmark.parentGuid), + bookmark.guid, + bookmark.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + ]); + await check_keyword(URI1, "keyword"); + Assert.equal(await foreign_count(URI1), fc + 2); // + 1 bookmark + 1 keyword + await check_orphans(); +}); + +add_task(async function test_addBookmarkToURIHavingKeyword() { + // The uri has already a keyword. + await check_keyword(URI1, "keyword"); + let fc = await foreign_count(URI1); + + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: URI1, + title: "test", + }); + await check_keyword(URI1, "keyword"); + Assert.equal(await foreign_count(URI1), fc + 1); // + 1 bookmark + await PlacesUtils.bookmarks.remove(bookmark); + await check_orphans(); +}); + +add_task(async function test_sameKeywordDifferentURI() { + let timerPrecision = Preferences.get("privacy.reduceTimerPrecision"); + Preferences.set("privacy.reduceTimerPrecision", false); + + registerCleanupFunction(function() { + Preferences.set("privacy.reduceTimerPrecision", timerPrecision); + }); + + let fc1 = await foreign_count(URI1); + let fc2 = await foreign_count(URI2); + let observer = expectNotifications(); + + let bookmark2 = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: URI2, + title: "test2", + }); + await check_keyword(URI1, "keyword"); + await check_keyword(URI2, null); + + await PlacesUtils.keywords.insert({ url: URI2, keyword: "kEyWoRd" }); + + let bookmark1 = await PlacesUtils.bookmarks.fetch({ url: URI1 }); + observer.check([ + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(bookmark1.guid), + "keyword", + false, + "", + bookmark1.lastModified * 1000, + bookmark1.type, + await PlacesUtils.promiseItemId(bookmark1.parentGuid), + bookmark1.guid, + bookmark1.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(bookmark2.guid), + "keyword", + false, + "keyword", + bookmark2.lastModified * 1000, + bookmark2.type, + await PlacesUtils.promiseItemId(bookmark2.parentGuid), + bookmark2.guid, + bookmark2.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + ]); + + // The keyword should have been "moved" to the new URI. + await check_keyword(URI1, null); + Assert.equal(await foreign_count(URI1), fc1 - 1); // - 1 keyword + await check_keyword(URI2, "keyword"); + Assert.equal(await foreign_count(URI2), fc2 + 2); // + 1 bookmark + 1 keyword + await check_orphans(); +}); + +add_task(async function test_sameURIDifferentKeyword() { + let fc = await foreign_count(URI2); + let observer = expectNotifications(); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: URI2, + title: "test2", + }); + await check_keyword(URI2, "keyword"); + + await PlacesUtils.keywords.insert({ url: URI2, keyword: "keyword2" }); + let bookmarks = []; + await PlacesUtils.bookmarks.fetch({ url: URI2 }, bm => bookmarks.push(bm)); + observer.check([ + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(bookmarks[0].guid), + "keyword", + false, + "keyword2", + bookmarks[0].lastModified * 1000, + bookmarks[0].type, + await PlacesUtils.promiseItemId(bookmarks[0].parentGuid), + bookmarks[0].guid, + bookmarks[0].parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(bookmarks[1].guid), + "keyword", + false, + "keyword2", + bookmarks[1].lastModified * 1000, + bookmarks[1].type, + await PlacesUtils.promiseItemId(bookmarks[1].parentGuid), + bookmarks[1].guid, + bookmarks[1].parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + ]); + await check_keyword(URI2, "keyword2"); + Assert.equal(await foreign_count(URI2), fc + 1); // + 1 bookmark - 1 keyword + 1 keyword + await check_orphans(); +}); + +add_task(async function test_removeBookmarkWithKeyword() { + let fc = await foreign_count(URI2); + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: URI2, + title: "test", + }); + + // The keyword should not be removed, since there are other bookmarks yet. + await PlacesUtils.bookmarks.remove(bookmark); + + await check_keyword(URI2, "keyword2"); + Assert.equal(await foreign_count(URI2), fc); // + 1 bookmark - 1 bookmark + await check_orphans(); +}); + +add_task(async function test_unsetKeyword() { + let fc = await foreign_count(URI2); + let observer = expectNotifications(); + + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: URI2, + title: "test", + }); + + // The keyword should be removed from any bookmark. + await PlacesUtils.keywords.remove("keyword2"); + + let bookmarks = []; + await PlacesUtils.bookmarks.fetch({ url: URI2 }, bookmark => + bookmarks.push(bookmark) + ); + Assert.equal(bookmarks.length, 3, "Check number of bookmarks"); + observer.check([ + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(bookmarks[0].guid), + "keyword", + false, + "", + bookmarks[0].lastModified * 1000, + bookmarks[0].type, + await PlacesUtils.promiseItemId(bookmarks[0].parentGuid), + bookmarks[0].guid, + bookmarks[0].parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(bookmarks[1].guid), + "keyword", + false, + "", + bookmarks[1].lastModified * 1000, + bookmarks[1].type, + await PlacesUtils.promiseItemId(bookmarks[1].parentGuid), + bookmarks[1].guid, + bookmarks[1].parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(bookmarks[2].guid), + "keyword", + false, + "", + bookmarks[2].lastModified * 1000, + bookmarks[2].type, + await PlacesUtils.promiseItemId(bookmarks[2].parentGuid), + bookmarks[2].guid, + bookmarks[2].parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + ]); + + await check_keyword(URI1, null); + await check_keyword(URI2, null); + Assert.equal(await foreign_count(URI2), fc); // + 1 bookmark - 1 keyword + await check_orphans(); +}); + +add_task(async function test_addRemoveBookmark() { + let fc = await foreign_count(URI3); + let observer = expectNotifications(); + + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: URI3, + title: "test3", + }); + let itemId = await PlacesUtils.promiseItemId(bookmark.guid); + await PlacesUtils.keywords.insert({ url: URI3, keyword: "keyword" }); + await PlacesUtils.bookmarks.remove(bookmark); + + observer.check([ + { + name: "onItemChanged", + arguments: [ + itemId, + "keyword", + false, + "keyword", + bookmark.lastModified * 1000, + bookmark.type, + await PlacesUtils.promiseItemId(bookmark.parentGuid), + bookmark.guid, + bookmark.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + ]); + + await check_keyword(URI3, null); + Assert.equal(await foreign_count(URI3), fc); // +- 1 bookmark +- 1 keyword + await check_orphans(); +}); + +add_task(async function test_reassign() { + // Should move keywords from old URL to new URL. + info("Old URL with keywords; new URL without keywords"); + { + let oldURL = "http://example.com/1/kw"; + let oldBmk = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: oldURL, + }); + await PlacesUtils.keywords.insert({ + url: oldURL, + keyword: "kw1-1", + postData: "a=b", + }); + await PlacesUtils.keywords.insert({ + url: oldURL, + keyword: "kw1-2", + postData: "c=d", + }); + let oldFC = await foreign_count(oldURL); + equal(oldFC, 3); + + let newURL = "http://example.com/2/no-kw"; + let newBmk = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: newURL, + }); + let newFC = await foreign_count(newURL); + equal(newFC, 1); + + let observer = expectNotifications(); + await PlacesUtils.keywords.reassign(oldURL, newURL); + observer.check([ + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(oldBmk.guid), + "keyword", + false, + "", + oldBmk.lastModified * 1000, + oldBmk.type, + await PlacesUtils.promiseItemId(oldBmk.parentGuid), + oldBmk.guid, + oldBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(newBmk.guid), + "keyword", + false, + "", + newBmk.lastModified * 1000, + newBmk.type, + await PlacesUtils.promiseItemId(newBmk.parentGuid), + newBmk.guid, + newBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(newBmk.guid), + "keyword", + false, + "kw1-1", + newBmk.lastModified * 1000, + newBmk.type, + await PlacesUtils.promiseItemId(newBmk.parentGuid), + newBmk.guid, + newBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(newBmk.guid), + "keyword", + false, + "kw1-2", + newBmk.lastModified * 1000, + newBmk.type, + await PlacesUtils.promiseItemId(newBmk.parentGuid), + newBmk.guid, + newBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + ]); + + await check_keyword(oldURL, null); + await check_keyword(newURL, "kw1-1"); + await check_keyword(newURL, "kw1-2"); + + equal(await foreign_count(oldURL), oldFC - 2); // Removed both keywords. + equal(await foreign_count(newURL), newFC + 2); // Added two keywords. + } + + // Should not remove any keywords from new URL. + info("Old URL without keywords; new URL with keywords"); + { + let oldURL = "http://example.com/3/no-kw"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: oldURL, + }); + let oldFC = await foreign_count(oldURL); + equal(oldFC, 1); + + let newURL = "http://example.com/4/kw"; + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: newURL, + }); + await PlacesUtils.keywords.insert({ + url: newURL, + keyword: "kw4-1", + }); + let newFC = await foreign_count(newURL); + equal(newFC, 2); + + let observer = expectNotifications(); + await PlacesUtils.keywords.reassign(oldURL, newURL); + observer.check([]); + + await check_keyword(newURL, "kw4-1"); + + equal(await foreign_count(oldURL), oldFC); + equal(await foreign_count(newURL), newFC); + } + + // Should remove all keywords from new URL, then move keywords from old URL. + info("Old URL with keywords; new URL with keywords"); + { + let oldURL = "http://example.com/8/kw"; + let oldBmk = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: oldURL, + }); + await PlacesUtils.keywords.insert({ + url: oldURL, + keyword: "kw8-1", + postData: "a=b", + }); + await PlacesUtils.keywords.insert({ + url: oldURL, + keyword: "kw8-2", + postData: "c=d", + }); + let oldFC = await foreign_count(oldURL); + equal(oldFC, 3); + + let newURL = "http://example.com/9/kw"; + let newBmk = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + url: newURL, + }); + await PlacesUtils.keywords.insert({ + url: newURL, + keyword: "kw9-1", + }); + let newFC = await foreign_count(newURL); + equal(newFC, 2); + + let observer = expectNotifications(); + await PlacesUtils.keywords.reassign(oldURL, newURL); + observer.check([ + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(oldBmk.guid), + "keyword", + false, + "", + oldBmk.lastModified * 1000, + oldBmk.type, + await PlacesUtils.promiseItemId(oldBmk.parentGuid), + oldBmk.guid, + oldBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(newBmk.guid), + "keyword", + false, + "", + newBmk.lastModified * 1000, + newBmk.type, + await PlacesUtils.promiseItemId(newBmk.parentGuid), + newBmk.guid, + newBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(newBmk.guid), + "keyword", + false, + "kw8-1", + newBmk.lastModified * 1000, + newBmk.type, + await PlacesUtils.promiseItemId(newBmk.parentGuid), + newBmk.guid, + newBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + { + name: "onItemChanged", + arguments: [ + await PlacesUtils.promiseItemId(newBmk.guid), + "keyword", + false, + "kw8-2", + newBmk.lastModified * 1000, + newBmk.type, + await PlacesUtils.promiseItemId(newBmk.parentGuid), + newBmk.guid, + newBmk.parentGuid, + "", + Ci.nsINavBookmarksService.SOURCE_DEFAULT, + ], + }, + ]); + + await check_keyword(oldURL, null); + await check_keyword(newURL, "kw8-1"); + await check_keyword(newURL, "kw8-2"); + + equal(await foreign_count(oldURL), oldFC - 2); // Removed both keywords. + equal(await foreign_count(newURL), newFC + 1); // Removed old keyword; added two keywords. + } + + // Should do nothing. + info("Old URL without keywords; new URL without keywords"); + { + let oldURL = "http://example.com/10/no-kw"; + let oldFC = await foreign_count(oldURL); + + let newURL = "http://example.com/11/no-kw"; + let newFC = await foreign_count(newURL); + + let observer = expectNotifications(); + await PlacesUtils.keywords.reassign(oldURL, newURL); + observer.check([]); + + equal(await foreign_count(oldURL), oldFC); + equal(await foreign_count(newURL), newFC); + } + + await check_orphans(); +}); + +add_task(async function test_invalidation() { + info("Insert bookmarks"); + let fx = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "Get Firefox!", + url: "http://getfirefox.com", + }); + let tb = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Get Thunderbird!", + url: "http://getthunderbird.com", + }); + + info("Set keywords for bookmarks"); + await PlacesUtils.keywords.insert({ url: fx.url, keyword: "fx" }); + await PlacesUtils.keywords.insert({ url: tb.url, keyword: "tb" }); + + info("Invalidate cached keywords"); + await PlacesUtils.keywords.invalidateCachedKeywords(); + + info("Change URL of bookmark with keyword"); + let promiseNotification = PlacesTestUtils.waitForNotification( + "onItemChanged", + (id, prop, isAnnoProp, newValue, lastModified, type, parentId, guid) => + guid == fx.guid && prop == "keyword" && newValue == "fx" + ); + await PlacesUtils.bookmarks.update({ + guid: fx.guid, + url: "https://www.mozilla.org/firefox", + }); + await promiseNotification; + + let entriesByKeyword = []; + await PlacesUtils.keywords.fetch({ keyword: "fx" }, e => + entriesByKeyword.push(e.url.href) + ); + deepEqual( + entriesByKeyword, + ["https://www.mozilla.org/firefox"], + "Should return new URL for keyword" + ); + + ok( + !(await PlacesUtils.keywords.fetch({ url: "http://getfirefox.com" })), + "Should not return keywords for old URL" + ); + + let entiresByURL = []; + await PlacesUtils.keywords.fetch( + { url: "https://www.mozilla.org/firefox" }, + e => entiresByURL.push(e.keyword) + ); + deepEqual(entiresByURL, ["fx"], "Should return keyword for new URL"); + + info("Invalidate cached keywords"); + await PlacesUtils.keywords.invalidateCachedKeywords(); + + info("Remove bookmark with keyword"); + await PlacesUtils.bookmarks.remove(tb.guid); + + ok( + !(await PlacesUtils.keywords.fetch({ url: "http://getthunderbird.com" })), + "Should not return keywords for removed bookmark URL" + ); + + ok( + !(await PlacesUtils.keywords.fetch({ keyword: "tb" })), + "Should not return URL for removed bookmark keyword" + ); + await check_orphans(); + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_eraseAllBookmarks() { + info("Insert bookmarks"); + let fx = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "Get Firefox!", + url: "http://getfirefox.com", + }); + let tb = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + title: "Get Thunderbird!", + url: "http://getthunderbird.com", + }); + + info("Set keywords for bookmarks"); + await PlacesUtils.keywords.insert({ url: fx.url, keyword: "fx" }); + await PlacesUtils.keywords.insert({ url: tb.url, keyword: "tb" }); + + info("Erase everything"); + await PlacesUtils.bookmarks.eraseEverything(); + + ok( + !(await PlacesUtils.keywords.fetch({ keyword: "fx" })), + "Should remove Firefox keyword" + ); + + ok( + !(await PlacesUtils.keywords.fetch({ keyword: "tb" })), + "Should remove Thunderbird keyword" + ); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js new file mode 100644 index 0000000000..1b99bf5502 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_nsINavBookmarkObserver.js @@ -0,0 +1,944 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +// Tests that each bookmark event gets the correct input. + +var gUnfiledFolderId; + +var gBookmarksObserver = { + expected: [], + setup(expected) { + this.expected = expected; + this.deferred = PromiseUtils.defer(); + return this.deferred.promise; + }, + + // Even though this isn't technically testing nsINavBookmarkObserver, + // this is the simplest place to keep this. Once all of the notifications + // are converted, we can just rename the file. + validateEvents(events) { + Assert.greaterOrEqual(this.expected.length, events.length); + for (let event of events) { + let expected = this.expected.shift(); + Assert.equal(expected.eventType, event.type); + let args = expected.args; + for (let i = 0; i < args.length; i++) { + Assert.ok( + args[i].check(event[args[i].name]), + event.type + "(args[" + i + "]: " + args[i].name + ")" + ); + } + } + + if (this.expected.length === 0) { + this.deferred.resolve(); + } + }, + + handlePlacesEvents(events) { + this.validateEvents(events); + }, +}; + +var gBookmarkSkipObserver = { + skipTags: true, + + expected: null, + setup(expected) { + this.expected = expected; + this.deferred = PromiseUtils.defer(); + return this.deferred.promise; + }, + + validateEvents(events) { + events = events.filter(e => !e.isTagging); + Assert.greaterOrEqual(this.expected.length, events.length); + for (let event of events) { + let expectedEventType = this.expected.shift(); + Assert.equal(expectedEventType, event.type); + } + + if (this.expected.length === 0) { + this.deferred.resolve(); + } + }, + + handlePlacesEvents(events) { + this.validateEvents(events); + }, +}; + +add_task(async function setup() { + gUnfiledFolderId = await PlacesUtils.promiseItemId( + PlacesUtils.bookmarks.unfiledGuid + ); + gBookmarksObserver.handlePlacesEvents = gBookmarksObserver.handlePlacesEvents.bind( + gBookmarksObserver + ); + gBookmarkSkipObserver.handlePlacesEvents = gBookmarkSkipObserver.handlePlacesEvents.bind( + gBookmarkSkipObserver + ); + PlacesUtils.observers.addListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-tags-changed", + "bookmark-title-changed", + ], + gBookmarksObserver.handlePlacesEvents + ); + PlacesUtils.observers.addListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-tags-changed", + "bookmark-title-changed", + ], + gBookmarkSkipObserver.handlePlacesEvents + ); +}); + +add_task(async function bookmarkItemAdded_bookmark() { + const title = "Bookmark 1"; + let uri = Services.io.newURI("http://1.mozilla.org/"); + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-added"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-added", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => v === gUnfiledFolderId }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v == uri.spec }, + { name: "title", check: v => v === title }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + url: uri, + title, + }); + await promise; +}); + +add_task(async function bookmarkItemAdded_separator() { + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-added"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-added", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => v === gUnfiledFolderId }, + { name: "index", check: v => v === 1 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v === "" }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }); + await promise; +}); + +add_task(async function bookmarkItemAdded_folder() { + const title = "Folder 1"; + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-added"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-added", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => v === gUnfiledFolderId }, + { name: "index", check: v => v === 2 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v === title }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + await promise; +}); + +add_task(async function bookmarkTitleChanged() { + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + const title = "New title"; + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-title-changed"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-title-changed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "title", check: v => v === title }, + { name: "lastModified", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.update({ guid: bm.guid, title }); + await promise; +}); + +add_task(async function bookmarkTagsChanged() { + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + let uri = Services.io.newURI(bm.url.href); + const TAG = "tag"; + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "bookmark-tags-changed", + "bookmark-tags-changed", + ]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-added", // This is the tag folder. + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.tagsFolderId }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v === TAG }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-added", // This is the tag. + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v == uri.spec }, + { name: "title", check: v => v === "" }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-tags-changed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { name: "lastModified", check: v => typeof v == "number" && v > 0 }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + { + name: "isTagging", + check: v => v === false, + }, + ], + }, + { + eventType: "bookmark-removed", // This is the tag. + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v == uri.spec }, + { name: "title", check: v => v == "" }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-tags-changed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { name: "lastModified", check: v => typeof v == "number" && v > 0 }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + { + name: "isTagging", + check: v => v === false, + }, + ], + }, + { + eventType: "bookmark-removed", // This is the tag folder. + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => v === PlacesUtils.tagsFolderId }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v == TAG }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + PlacesUtils.tagging.tagURI(uri, [TAG]); + PlacesUtils.tagging.untagURI(uri, [TAG]); + await promise; +}); + +add_task(async function bookmarkItemMoved_bookmark() { + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-moved", "bookmark-moved"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-moved", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "oldIndex", check: v => v === 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "oldParentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + { name: "url", check: v => typeof v == "string" }, + ], + }, + { + eventType: "bookmark-moved", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "oldIndex", check: v => v === 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "oldParentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + { name: "url", check: v => typeof v == "string" }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + index: 0, + }); + await PlacesUtils.bookmarks.update({ + guid: bm.guid, + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + await promise; +}); + +add_task(async function bookmarkItemRemoved_bookmark() { + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + let uri = Services.io.newURI(bm.url.href); + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-removed"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-removed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => v === gUnfiledFolderId }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v === uri.spec }, + { name: "title", check: v => v == "New title" }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.remove(bm); + await promise; +}); + +add_task(async function bookmarkItemRemoved_separator() { + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-removed"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-removed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v == "" }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.remove(bm); + await promise; +}); + +add_task(async function bookmarkItemRemoved_folder() { + let bm = await PlacesUtils.bookmarks.fetch({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + index: 0, + }); + let promise = Promise.all([ + gBookmarkSkipObserver.setup(["bookmark-removed"]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-removed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v == "Folder 1" }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + await PlacesUtils.bookmarks.remove(bm); + await promise; +}); + +add_task(async function bookmarkItemRemoved_folder_recursive() { + const title = "Folder 3"; + const BMTITLE = "Bookmark 1"; + let uri = Services.io.newURI("http://1.mozilla.org/"); + let promise = Promise.all([ + gBookmarkSkipObserver.setup([ + "bookmark-added", + "bookmark-added", + "bookmark-added", + "bookmark-added", + "bookmark-removed", + "bookmark-removed", + "bookmark-removed", + "bookmark-removed", + ]), + gBookmarksObserver.setup([ + { + eventType: "bookmark-added", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => v === gUnfiledFolderId }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v === title }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-added", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v == uri.spec }, + { name: "title", check: v => v === BMTITLE }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-added", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 1 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v === title }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-added", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v == uri.spec }, + { name: "title", check: v => v === BMTITLE }, + { name: "dateAdded", check: v => typeof v == "number" && v > 0 }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-removed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v === uri.spec }, + { name: "title", check: v => v == BMTITLE }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-removed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 1 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v == title }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-removed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_BOOKMARK, + }, + { name: "url", check: v => v === uri.spec }, + { name: "title", check: v => v == BMTITLE }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + { + eventType: "bookmark-removed", + args: [ + { name: "id", check: v => typeof v == "number" && v > 0 }, + { name: "parentId", check: v => typeof v == "number" && v > 0 }, + { name: "index", check: v => v === 0 }, + { + name: "itemType", + check: v => v === PlacesUtils.bookmarks.TYPE_FOLDER, + }, + { name: "url", check: v => v === "" }, + { name: "title", check: v => v == title }, + { + name: "guid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "parentGuid", + check: v => typeof v == "string" && PlacesUtils.isValidGuid(v), + }, + { + name: "source", + check: v => + Object.values(PlacesUtils.bookmarks.SOURCES).includes(v), + }, + ], + }, + ]), + ]); + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + title, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + url: uri, + title: BMTITLE, + }); + let folder2 = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + title, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + await PlacesUtils.bookmarks.insert({ + parentGuid: folder2.guid, + url: uri, + title: BMTITLE, + }); + + await PlacesUtils.bookmarks.remove(folder); + await promise; +}); + +add_task(function cleanup() { + PlacesUtils.observers.removeListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-tags-changed", + "bookmark-title-changed", + ], + gBookmarksObserver.handlePlacesEvents + ); + PlacesUtils.observers.removeListener( + [ + "bookmark-added", + "bookmark-removed", + "bookmark-moved", + "bookmark-tags-changed", + "bookmark-title-changed", + ], + gBookmarkSkipObserver.handlePlacesEvents + ); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js new file mode 100644 index 0000000000..2c9b5c44aa --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js @@ -0,0 +1,118 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ +/** + * This test ensures that reinserting a folder within a transaction gives it + * the same GUID, and passes it to the observers. + */ + +add_task(async function test_removeFolderTransaction_reinsert() { + let folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: "Test folder", + }); + let fx = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + title: "Get Firefox!", + url: "http://getfirefox.com", + }); + let tb = await PlacesUtils.bookmarks.insert({ + parentGuid: folder.guid, + title: "Get Thunderbird!", + url: "http://getthunderbird.com", + }); + + let notifications = []; + function checkNotifications(expected, message) { + deepEqual(notifications, expected, message); + notifications.length = 0; + } + + let listener = events => { + for (let event of events) { + notifications.push([ + event.type, + event.id, + event.parentId, + event.guid, + event.parentGuid, + ]); + } + }; + let observer = Object.create(NavBookmarkObserver.prototype); + PlacesUtils.bookmarks.addObserver(observer); + PlacesUtils.observers.addListener( + ["bookmark-added", "bookmark-removed"], + listener + ); + PlacesUtils.registerShutdownFunction(function() { + PlacesUtils.bookmarks.removeObserver(observer); + PlacesUtils.observers.removeListener( + ["bookmark-added", "bookmark-removed"], + listener + ); + }); + + let transaction = PlacesTransactions.Remove({ guid: folder.guid }); + + let folderId = await PlacesUtils.promiseItemId(folder.guid); + let fxId = await PlacesUtils.promiseItemId(fx.guid); + let tbId = await PlacesUtils.promiseItemId(tb.guid); + + await transaction.transact(); + let bookmarksMenuItemId = await PlacesUtils.promiseItemId( + PlacesUtils.bookmarks.menuGuid + ); + checkNotifications( + [ + ["bookmark-removed", tbId, folderId, tb.guid, folder.guid], + ["bookmark-removed", fxId, folderId, fx.guid, folder.guid], + [ + "bookmark-removed", + folderId, + bookmarksMenuItemId, + folder.guid, + PlacesUtils.bookmarks.menuGuid, + ], + ], + "Executing transaction should remove folder and its descendants" + ); + + await PlacesTransactions.undo(); + + folderId = await PlacesUtils.promiseItemId(folder.guid); + fxId = await PlacesUtils.promiseItemId(fx.guid); + tbId = await PlacesUtils.promiseItemId(tb.guid); + + checkNotifications( + [ + [ + "bookmark-added", + folderId, + bookmarksMenuItemId, + folder.guid, + PlacesUtils.bookmarks.menuGuid, + ], + ["bookmark-added", fxId, folderId, fx.guid, folder.guid], + ["bookmark-added", tbId, folderId, tb.guid, folder.guid], + ], + "Undo should reinsert folder with different id but same GUID" + ); + + await PlacesTransactions.redo(); + + checkNotifications( + [ + ["bookmark-removed", tbId, folderId, tb.guid, folder.guid], + ["bookmark-removed", fxId, folderId, fx.guid, folder.guid], + [ + "bookmark-removed", + folderId, + bookmarksMenuItemId, + folder.guid, + PlacesUtils.bookmarks.menuGuid, + ], + ], + "Redo should pass the GUID to observer" + ); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_savedsearches.js b/toolkit/components/places/tests/bookmarks/test_savedsearches.js new file mode 100644 index 0000000000..a10307983d --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_savedsearches.js @@ -0,0 +1,224 @@ +/* -*- 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/. */ + +// a search term that matches a default bookmark +const searchTerm = "about"; + +var testRoot; + +add_task(async function setup() { + // create a folder to hold all the tests + // this makes the tests more tolerant of changes to the default bookmarks set + // also, name it using the search term, for testing that containers that match don't show up in query results + testRoot = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: searchTerm, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); +}); + +add_task(async function test_savedsearches_bookmarks() { + // add a bookmark that matches the search term + let bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: searchTerm, + url: "http://foo.com", + }); + + // create a saved-search that matches a default bookmark + let search = await PlacesUtils.bookmarks.insert({ + parentGuid: testRoot.guid, + title: searchTerm, + url: + "place:terms=" + + searchTerm + + "&excludeQueries=1&expandQueries=1&queryType=1", + }); + + // query for the test root, expandQueries=0 + // the query should show up as a regular bookmark + try { + let options = PlacesUtils.history.getNewQueryOptions(); + options.expandQueries = 0; + let query = PlacesUtils.history.getNewQuery(); + query.setParents([testRoot.guid]); + let result = PlacesUtils.history.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + Assert.equal(cc, 1); + for (let i = 0; i < cc; i++) { + let node = rootNode.getChild(i); + // test that queries have valid itemId + Assert.ok(node.itemId > 0); + // test that the container is closed + node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + Assert.equal(node.containerOpen, false); + } + rootNode.containerOpen = false; + } catch (ex) { + do_throw("expandQueries=0 query error: " + ex); + } + + // bookmark saved search + // query for the test root, expandQueries=1 + // the query should show up as a query container, with 1 child + try { + let options = PlacesUtils.history.getNewQueryOptions(); + options.expandQueries = 1; + let query = PlacesUtils.history.getNewQuery(); + query.setParents([testRoot.guid]); + let result = PlacesUtils.history.executeQuery(query, options); + let rootNode = result.root; + rootNode.containerOpen = true; + let cc = rootNode.childCount; + Assert.equal(cc, 1); + for (let i = 0; i < cc; i++) { + let node = rootNode.getChild(i); + // test that query node type is container when expandQueries=1 + Assert.equal(node.type, node.RESULT_TYPE_QUERY); + // test that queries (as containers) have valid itemId + Assert.ok(node.itemId > 0); + node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + node.containerOpen = true; + + // test that queries have children when excludeItems=1 + // test that query nodes don't show containers (shouldn't have our folder that matches) + // test that queries don't show themselves in query results (shouldn't have our saved search) + Assert.equal(node.childCount, 1); + + // test that bookmark shows in query results + var item = node.getChild(0); + Assert.equal(item.bookmarkGuid, bookmark.guid); + + // XXX - FAILING - test live-update of query results - add a bookmark that matches the query + // var tmpBmId = PlacesUtils.bookmarks.insertBookmark( + // root, uri("http://" + searchTerm + ".com"), + // PlacesUtils.bookmarks.DEFAULT_INDEX, searchTerm + "blah"); + // do_check_eq(query.childCount, 2); + + // XXX - test live-update of query results - delete a bookmark that matches the query + // PlacesUtils.bookmarks.removeItem(tmpBMId); + // do_check_eq(query.childCount, 1); + + // test live-update of query results - add a folder that matches the query + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: searchTerm + "zaa", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + Assert.equal(node.childCount, 1); + // test live-update of query results - add a query that matches the query + await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.menuGuid, + title: searchTerm + "blah", + url: "place:terms=foo&excludeQueries=1&expandQueries=1&queryType=1", + }); + Assert.equal(node.childCount, 1); + } + rootNode.containerOpen = false; + } catch (ex) { + do_throw("expandQueries=1 bookmarks query: " + ex); + } + + // delete the bookmark search + await PlacesUtils.bookmarks.remove(search); +}); + +add_task(async function test_savedsearches_history() { + // add a visit that matches the search term + var testURI = uri("http://" + searchTerm + ".com"); + await PlacesTestUtils.addVisits({ uri: testURI, title: searchTerm }); + + // create a saved-search that matches the visit we added + var searchItem = await PlacesUtils.bookmarks.insert({ + parentGuid: testRoot.guid, + title: searchTerm, + url: + "place:terms=" + + searchTerm + + "&excludeQueries=1&expandQueries=1&queryType=0", + }); + + // query for the test root, expandQueries=1 + // the query should show up as a query container, with 1 child + try { + var options = PlacesUtils.history.getNewQueryOptions(); + options.expandQueries = 1; + var query = PlacesUtils.history.getNewQuery(); + query.setParents([testRoot.guid]); + var result = PlacesUtils.history.executeQuery(query, options); + var rootNode = result.root; + rootNode.containerOpen = true; + var cc = rootNode.childCount; + Assert.equal(cc, 1); + for (var i = 0; i < cc; i++) { + var node = rootNode.getChild(i); + // test that query node type is container when expandQueries=1 + Assert.equal(node.type, node.RESULT_TYPE_QUERY); + // test that queries (as containers) have valid itemId + Assert.equal(node.bookmarkGuid, searchItem.guid); + node.QueryInterface(Ci.nsINavHistoryContainerResultNode); + node.containerOpen = true; + + // test that queries have children when excludeItems=1 + // test that query nodes don't show containers (shouldn't have our folder that matches) + // test that queries don't show themselves in query results (shouldn't have our saved search) + Assert.equal(node.childCount, 1); + + // test that history visit shows in query results + var item = node.getChild(0); + Assert.equal(item.type, item.RESULT_TYPE_URI); + Assert.equal(item.itemId, -1); // history visit + Assert.equal(item.uri, testURI.spec); // history visit + + // test live-update of query results - add a history visit that matches the query + await PlacesTestUtils.addVisits({ + uri: uri("http://foo.com"), + title: searchTerm + "blah", + }); + Assert.equal(node.childCount, 2); + + // test live-update of query results - delete a history visit that matches the query + await PlacesUtils.history.remove("http://foo.com"); + Assert.equal(node.childCount, 1); + node.containerOpen = false; + } + + // test live-update of moved queries + let tmpFolder = await PlacesUtils.bookmarks.insert({ + parentGuid: testRoot.guid, + title: "foo", + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + searchItem.parentGuid = tmpFolder.guid; + await PlacesUtils.bookmarks.update(searchItem); + var tmpFolderNode = rootNode.getChild(0); + Assert.equal(tmpFolderNode.bookmarkGuid, tmpFolder.guid); + tmpFolderNode.QueryInterface(Ci.nsINavHistoryContainerResultNode); + tmpFolderNode.containerOpen = true; + Assert.equal(tmpFolderNode.childCount, 1); + + // test live-update of renamed queries + searchItem.title = "foo"; + await PlacesUtils.bookmarks.update(searchItem); + Assert.equal(tmpFolderNode.title, "foo"); + + // test live-update of deleted queries + await PlacesUtils.bookmarks.remove(searchItem); + Assert.throws( + () => (tmpFolderNode = rootNode.getChild(1)), + /NS_ERROR_ILLEGAL_VALUE/, + "getting a deleted child should throw" + ); + + tmpFolderNode.containerOpen = false; + rootNode.containerOpen = false; + } catch (ex) { + do_throw("expandQueries=1 bookmarks query: " + ex); + } +}); diff --git a/toolkit/components/places/tests/bookmarks/test_sync_fields.js b/toolkit/components/places/tests/bookmarks/test_sync_fields.js new file mode 100644 index 0000000000..7db76e96e6 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_sync_fields.js @@ -0,0 +1,438 @@ +// Tracks a set of bookmark guids and their syncChangeCounter field and +// provides a simple way for the test to check the correct fields had the +// counter incremented. +class CounterTracker { + constructor() { + this.tracked = new Map(); + } + + async _getCounter(guid) { + let fields = await PlacesTestUtils.fetchBookmarkSyncFields(guid); + if (!fields.length) { + throw new Error(`Item ${guid} does not exist`); + } + return fields[0].syncChangeCounter; + } + + // Call this after creating a new bookmark. + async track(guid, name, expectedInitial = 1) { + if (this.tracked.has(guid)) { + throw new Error(`Already tracking item ${guid}`); + } + let initial = await this._getCounter(guid); + Assert.equal( + initial, + expectedInitial, + `Initial value of item '${name}' is correct` + ); + this.tracked.set(guid, { name, value: expectedInitial }); + } + + // Call this to check *only* the specified IDs had a change increment, and + // that none of the other "tracked" ones did. + async check(...expectedToIncrement) { + info(`Checking counter for items ${JSON.stringify(expectedToIncrement)}`); + for (let [guid, entry] of this.tracked) { + let { name, value } = entry; + let newValue = await this._getCounter(guid); + let desc = `record '${name}' (guid=${guid})`; + if (expectedToIncrement.includes(guid)) { + // Note we don't check specifically for +1, as some changes will + // increment the counter by more than 1 (which is OK). + Assert.ok( + newValue > value, + `${desc} was expected to increment - was ${value}, now ${newValue}` + ); + this.tracked.set(guid, { name, value: newValue }); + } else { + Assert.equal(newValue, value, `${desc} was NOT expected to increment`); + } + } + } +} + +async function checkSyncFields(guid, expected) { + let results = await PlacesTestUtils.fetchBookmarkSyncFields(guid); + if (!results.length) { + throw new Error(`Missing sync fields for ${guid}`); + } + for (let name in expected) { + let expectedValue = expected[name]; + Assert.equal( + results[0][name], + expectedValue, + `field ${name} matches item ${guid}` + ); + } +} + +// Common test cases for sync field changes. +class TestCases { + async run() { + info("Test 1: inserts, updates, tags, and keywords"); + try { + await this.testChanges(); + } finally { + info("Reset sync fields after test 1"); + await PlacesTestUtils.markBookmarksAsSynced(); + } + + if ("moveItem" in this && "reorder" in this) { + info("Test 2: reparenting"); + try { + await this.testReparenting(); + } finally { + info("Reset sync fields after test 2"); + await PlacesTestUtils.markBookmarksAsSynced(); + } + } + + if ("insertSeparator" in this) { + info("Test 3: separators"); + try { + await this.testSeparators(); + } finally { + info("Reset sync fields after test 3"); + await PlacesTestUtils.markBookmarksAsSynced(); + } + } + } + + async testChanges() { + let testUri = NetUtil.newURI("http://test.mozilla.org"); + + let guid = await this.insertBookmark( + PlacesUtils.bookmarks.unfiledGuid, + testUri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark title" + ); + info(`Inserted bookmark ${guid}`); + await checkSyncFields(guid, { + syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NEW, + syncChangeCounter: 1, + }); + + // Pretend Sync just did whatever it does + await PlacesTestUtils.setBookmarkSyncFields({ + guid, + syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL, + }); + info(`Updated sync status of ${guid}`); + await checkSyncFields(guid, { + syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL, + syncChangeCounter: 1, + }); + + // update it - it should increment the change counter + await this.setTitle(guid, "new title"); + info(`Changed title of ${guid}`); + await checkSyncFields(guid, { + syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL, + syncChangeCounter: 2, + }); + + // Tagging a bookmark should update its change counter. + await this.tagURI(testUri, ["test-tag"]); + info(`Tagged bookmark ${guid}`); + await checkSyncFields(guid, { syncChangeCounter: 3 }); + + if ("setKeyword" in this) { + await this.setKeyword(guid, "keyword"); + info(`Set keyword for bookmark ${guid}`); + await checkSyncFields(guid, { syncChangeCounter: 4 }); + } + if ("removeKeyword" in this) { + await this.removeKeyword(guid, "keyword"); + info(`Removed keyword from bookmark ${guid}`); + await checkSyncFields(guid, { syncChangeCounter: 5 }); + } + } + + async testSeparators() { + let insertSyncedBookmark = uri => { + return this.insertBookmark( + PlacesUtils.bookmarks.unfiledGuid, + NetUtil.newURI(uri), + PlacesUtils.bookmarks.DEFAULT_INDEX, + "A bookmark name" + ); + }; + + await insertSyncedBookmark("http://foo.bar"); + let secondBmk = await insertSyncedBookmark("http://bar.foo"); + let sepGuid = await this.insertSeparator( + PlacesUtils.bookmarks.unfiledGuid, + PlacesUtils.bookmarks.DEFAULT_INDEX + ); + await insertSyncedBookmark("http://barbar.foo"); + + info("Move a bookmark around the separator"); + await this.moveItem(secondBmk, PlacesUtils.bookmarks.unfiledGuid, 4); + await checkSyncFields(sepGuid, { syncChangeCounter: 2 }); + + info("Move a separator around directly"); + await this.moveItem(sepGuid, PlacesUtils.bookmarks.unfiledGuid, 0); + await checkSyncFields(sepGuid, { syncChangeCounter: 3 }); + } + + async testReparenting() { + let counterTracker = new CounterTracker(); + + let folder1 = await this.createFolder( + PlacesUtils.bookmarks.unfiledGuid, + "folder1", + PlacesUtils.bookmarks.DEFAULT_INDEX + ); + info(`Created the first folder, guid is ${folder1}`); + + // New folder should have a change recorded. + await counterTracker.track(folder1, "folder 1"); + + // Put a new bookmark in the folder. + let testUri = NetUtil.newURI("http://test2.mozilla.org"); + let child1 = await this.insertBookmark( + folder1, + testUri, + PlacesUtils.bookmarks.DEFAULT_INDEX, + "bookmark 1" + ); + info(`Created a new bookmark into ${folder1}, guid is ${child1}`); + // both the folder and the child should have a change recorded. + await counterTracker.track(child1, "child 1"); + await counterTracker.check(folder1); + + // A new child in the folder at index 0 - even though the existing child + // was bumped down the list, it should *not* have a change recorded. + let child2 = await this.insertBookmark(folder1, testUri, 0, "bookmark 2"); + info( + `Created a second new bookmark into folder ${folder1}, guid is ${child2}` + ); + + await counterTracker.track(child2, "child 2"); + await counterTracker.check(folder1); + + // Move the items within the same folder - this should result in just a + // change for the parent, but for neither of the children. + // child0 is currently at index 0, so move child1 there. + await this.moveItem(child1, folder1, 0); + await counterTracker.check(folder1); + + // Another folder to play with. + let folder2 = await this.createFolder( + PlacesUtils.bookmarks.unfiledGuid, + "folder2", + PlacesUtils.bookmarks.DEFAULT_INDEX + ); + info(`Created a second new folder, guid is ${folder2}`); + await counterTracker.track(folder2, "folder 2"); + // nothing else has changed. + await counterTracker.check(); + + // Move one of the children to the new folder. + info( + `Moving bookmark ${child2} from folder ${folder1} to folder ${folder2}` + ); + await this.moveItem(child2, folder2, PlacesUtils.bookmarks.DEFAULT_INDEX); + // child1 should have no change, everything should have a new change. + await counterTracker.check(folder1, folder2, child2); + + // Move the new folder to another root. + await this.moveItem( + folder2, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.DEFAULT_INDEX + ); + info(`Moving folder ${folder2} to toolbar`); + await counterTracker.check( + folder2, + PlacesUtils.bookmarks.toolbarGuid, + PlacesUtils.bookmarks.unfiledGuid + ); + + let child3 = await this.insertBookmark(folder2, testUri, 0, "bookmark 3"); + info(`Prepended child ${child3} to folder ${folder2}`); + await counterTracker.check(folder2, child3); + + // Reordering should only track the parent. + await this.reorder(folder2, [child2, child3]); + info(`Reorder children of ${folder2}`); + await counterTracker.check(folder2); + + // All fields still have a syncStatus of SYNC_STATUS_NEW - so deleting them + // should *not* cause any deleted items to be written. + await this.removeItem(folder1); + Assert.equal((await PlacesTestUtils.fetchSyncTombstones()).length, 0); + + // Set folder2 and child2 to have a state of SYNC_STATUS_NORMAL and deleting + // them will cause both GUIDs to be written to moz_bookmarks_deleted. + await PlacesTestUtils.setBookmarkSyncFields({ + guid: folder2, + syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL, + }); + await PlacesTestUtils.setBookmarkSyncFields({ + guid: child2, + syncStatus: PlacesUtils.bookmarks.SYNC_STATUS.NORMAL, + }); + await this.removeItem(folder2); + let tombstones = await PlacesTestUtils.fetchSyncTombstones(); + let tombstoneGuids = sortBy(tombstones, "guid").map(({ guid }) => guid); + Assert.equal(tombstoneGuids.length, 2); + Assert.deepEqual(tombstoneGuids, [folder2, child2].sort(compareAscending)); + } +} + +// Exercises the legacy, synchronous `nsINavBookmarksService` calls implemented +// in C++. +class SyncTestCases extends TestCases { + async createFolder(parentGuid, title, index) { + let parentId = await PlacesUtils.promiseItemId(parentGuid); + let id = PlacesUtils.bookmarks.createFolder(parentId, title, index); + return PlacesUtils.promiseItemGuid(id); + } + + async insertBookmark(parentGuid, uri, index, title) { + let parentId = await PlacesUtils.promiseItemId(parentGuid); + let id = PlacesUtils.bookmarks.insertBookmark(parentId, uri, index, title); + return PlacesUtils.promiseItemGuid(id); + } + + async removeItem(guid) { + let id = await PlacesUtils.promiseItemId(guid); + PlacesUtils.bookmarks.removeItem(id); + } + + async setTitle(guid, title) { + let id = await PlacesUtils.promiseItemId(guid); + PlacesUtils.bookmarks.setItemTitle(id, title); + } + + async tagURI(uri, tags) { + PlacesUtils.tagging.tagURI(uri, tags); + } +} + +async function findTagFolder(tag) { + let db = await PlacesUtils.promiseDBConnection(); + let results = await db.executeCached( + ` + SELECT guid + FROM moz_bookmarks + WHERE type = :type AND + parent = :tagsFolderId AND + title = :tag`, + { + type: PlacesUtils.bookmarks.TYPE_FOLDER, + tagsFolderId: PlacesUtils.tagsFolderId, + tag, + } + ); + return results.length ? results[0].getResultByName("guid") : null; +} + +// Exercises the new, async calls implemented in `Bookmarks.jsm`. +class AsyncTestCases extends TestCases { + async createFolder(parentGuid, title, index) { + let item = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid, + title, + index, + }); + return item.guid; + } + + async insertBookmark(parentGuid, uri, index, title) { + let item = await PlacesUtils.bookmarks.insert({ + parentGuid, + url: uri, + index, + title, + }); + return item.guid; + } + + async insertSeparator(parentGuid, index) { + let item = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + parentGuid, + index, + }); + return item.guid; + } + + async moveItem(guid, newParentGuid, index) { + await PlacesUtils.bookmarks.update({ + guid, + parentGuid: newParentGuid, + index, + }); + } + + async removeItem(guid) { + await PlacesUtils.bookmarks.remove(guid); + } + + async setTitle(guid, title) { + await PlacesUtils.bookmarks.update({ guid, title }); + } + + async setKeyword(guid, keyword) { + let item = await PlacesUtils.bookmarks.fetch(guid); + if (!item) { + throw new Error( + `Cannot set keyword ${keyword} on nonexistent bookmark ${guid}` + ); + } + await PlacesUtils.keywords.insert({ keyword, url: item.url }); + } + + async removeKeyword(guid, keyword) { + let item = await PlacesUtils.bookmarks.fetch(guid); + if (!item) { + throw new Error( + `Cannot remove keyword ${keyword} from nonexistent bookmark ${guid}` + ); + } + let entry = await PlacesUtils.keywords.fetch({ keyword, url: item.url }); + if (!entry) { + throw new Error(`Keyword ${keyword} not set on bookmark ${guid}`); + } + await PlacesUtils.keywords.remove(entry); + } + + // There's no async API for tags, but the `PlacesUtils.bookmarks` methods are + // tag-aware, and should bump the change counters for tagged bookmarks when + // called directly. + async tagURI(uri, tags) { + for (let tag of tags) { + let tagFolderGuid = await findTagFolder(tag); + if (!tagFolderGuid) { + let tagFolder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + parentGuid: PlacesUtils.bookmarks.tagsGuid, + title: tag, + }); + tagFolderGuid = tagFolder.guid; + } + await PlacesUtils.bookmarks.insert({ + url: uri, + parentGuid: tagFolderGuid, + }); + } + } + + async reorder(parentGuid, childGuids) { + await PlacesUtils.bookmarks.reorder(parentGuid, childGuids); + } +} + +add_task(async function test_sync_api() { + let tests = new SyncTestCases(); + await tests.run(); +}); + +add_task(async function test_async_api() { + let tests = new AsyncTestCases(); + await tests.run(); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_tags.js b/toolkit/components/places/tests/bookmarks/test_tags.js new file mode 100644 index 0000000000..d230e204e9 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_tags.js @@ -0,0 +1,131 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +add_task(async function test_fetchTags() { + let tags = await PlacesUtils.bookmarks.fetchTags(); + Assert.deepEqual(tags, []); + + let bm = await PlacesUtils.bookmarks.insert({ + url: "http://page1.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + + PlacesUtils.tagging.tagURI(Services.io.newURI(bm.url.href), ["1", "2"]); + tags = await PlacesUtils.bookmarks.fetchTags(); + Assert.deepEqual(tags, [ + { name: "1", count: 1 }, + { name: "2", count: 1 }, + ]); + + PlacesUtils.tagging.untagURI(Services.io.newURI(bm.url.href), ["1"]); + tags = await PlacesUtils.bookmarks.fetchTags(); + Assert.deepEqual(tags, [{ name: "2", count: 1 }]); + + let bm2 = await PlacesUtils.bookmarks.insert({ + url: "http://page2.com/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(bm2.url.href), ["2", "3"]); + tags = await PlacesUtils.bookmarks.fetchTags(); + Assert.deepEqual(tags, [ + { name: "2", count: 2 }, + { name: "3", count: 1 }, + ]); +}); + +add_task(async function test_fetch_by_tags() { + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ tags: "" }), + /Invalid value for property 'tags'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ tags: [] }), + /Invalid value for property 'tags'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ tags: null }), + /Invalid value for property 'tags'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ tags: [""] }), + /Invalid value for property 'tags'/ + ); + Assert.throws( + () => PlacesUtils.bookmarks.fetch({ tags: ["valid", null] }), + /Invalid value for property 'tags'/ + ); + + info("Add bookmarks with tags."); + let bm1 = await PlacesUtils.bookmarks.insert({ + url: "http://bacon.org/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(bm1.url.href), [ + "egg", + "ratafià", + ]); + let bm2 = await PlacesUtils.bookmarks.insert({ + url: "http://mushroom.org/", + parentGuid: PlacesUtils.bookmarks.unfiledGuid, + }); + PlacesUtils.tagging.tagURI(Services.io.newURI(bm2.url.href), ["egg"]); + + info("Fetch a single tag."); + let bms = []; + Assert.equal( + (await PlacesUtils.bookmarks.fetch({ tags: ["egg"] }, b => bms.push(b))) + .guid, + bm2.guid, + "Found the expected recent bookmark" + ); + Assert.deepEqual( + bms.map(b => b.guid), + [bm2.guid, bm1.guid], + "Found the expected bookmarks" + ); + + info("Fetch multiple tags."); + bms = []; + Assert.equal( + ( + await PlacesUtils.bookmarks.fetch({ tags: ["egg", "ratafià"] }, b => + bms.push(b) + ) + ).guid, + bm1.guid, + "Found the expected recent bookmark" + ); + Assert.deepEqual( + bms.map(b => b.guid), + [bm1.guid], + "Found the expected bookmarks" + ); + + info("Fetch a nonexisting tag."); + bms = []; + Assert.equal( + await PlacesUtils.bookmarks.fetch({ tags: ["egg", "tomato"] }, b => + bms.push(b) + ), + null, + "Should not find any bookmark" + ); + Assert.deepEqual(bms, [], "Should not find any bookmark"); + + info("Check case insensitive"); + bms = []; + Assert.equal( + ( + await PlacesUtils.bookmarks.fetch({ tags: ["eGg", "raTafiÀ"] }, b => + bms.push(b) + ) + ).guid, + bm1.guid, + "Found the expected recent bookmark" + ); + Assert.deepEqual( + bms.map(b => b.guid), + [bm1.guid], + "Found the expected bookmarks" + ); +}); diff --git a/toolkit/components/places/tests/bookmarks/test_untitled.js b/toolkit/components/places/tests/bookmarks/test_untitled.js new file mode 100644 index 0000000000..6e756d79a6 --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/test_untitled.js @@ -0,0 +1,114 @@ +add_task(async function test_untitled_visited_bookmark() { + let fxURI = uri("http://getfirefox.com"); + + await PlacesUtils.history.insert({ + url: fxURI, + title: "Get Firefox!", + visits: [ + { + date: new Date(), + transition: PlacesUtils.history.TRANSITIONS.TYPED, + }, + ], + }); + + let { root: node } = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid + ); + + try { + let fxBmk = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: fxURI, + }); + strictEqual(fxBmk.title, "", "Visited bookmark should not have title"); + + await PlacesTestUtils.promiseAsyncUpdates(); + + let fxBmkId = await PlacesUtils.promiseItemId(fxBmk.guid); + strictEqual( + PlacesUtils.bookmarks.getItemTitle(fxBmkId), + "", + "Should return empty string for untitled visited bookmark" + ); + + let fxBmkNode = node.getChild(0); + equal(fxBmkNode.itemId, fxBmkId, "Visited bookmark ID should match"); + strictEqual( + fxBmkNode.title, + "", + "Visited bookmark node should not have title" + ); + } finally { + node.containerOpen = false; + } + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_untitled_unvisited_bookmark() { + let tbURI = uri("http://getthunderbird.com"); + + let { root: node } = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid + ); + + try { + let tbBmk = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: tbURI, + }); + strictEqual(tbBmk.title, "", "Unvisited bookmark should not have title"); + + await PlacesTestUtils.promiseAsyncUpdates(); + + let tbBmkId = await PlacesUtils.promiseItemId(tbBmk.guid); + strictEqual( + PlacesUtils.bookmarks.getItemTitle(tbBmkId), + "", + "Should return empty string for untitled unvisited bookmark" + ); + + let tbBmkNode = node.getChild(0); + equal(tbBmkNode.itemId, tbBmkId, "Unvisited bookmark ID should match"); + strictEqual( + tbBmkNode.title, + "", + "Unvisited bookmark node should not have title" + ); + } finally { + node.containerOpen = false; + } + + await PlacesUtils.bookmarks.eraseEverything(); +}); + +add_task(async function test_untitled_folder() { + let { root: node } = PlacesUtils.getFolderContents( + PlacesUtils.bookmarks.toolbarGuid + ); + + try { + let folder = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + type: PlacesUtils.bookmarks.TYPE_FOLDER, + }); + + await PlacesTestUtils.promiseAsyncUpdates(); + + let folderId = await PlacesUtils.promiseItemId(folder.guid); + strictEqual( + PlacesUtils.bookmarks.getItemTitle(folderId), + "", + "Should return empty string for untitled folder" + ); + + let folderNode = node.getChild(0); + equal(folderNode.itemId, folderId, "Folder ID should match"); + strictEqual(folderNode.title, "", "Folder node should not have title"); + } finally { + node.containerOpen = false; + } + + await PlacesUtils.bookmarks.eraseEverything(); +}); diff --git a/toolkit/components/places/tests/bookmarks/xpcshell.ini b/toolkit/components/places/tests/bookmarks/xpcshell.ini new file mode 100644 index 0000000000..597cfd780d --- /dev/null +++ b/toolkit/components/places/tests/bookmarks/xpcshell.ini @@ -0,0 +1,48 @@ +[DEFAULT] +head = head_bookmarks.js +skip-if = toolkit == 'android' +firefox-appdir = browser + +[test_1016953-renaming-uncompressed.js] +[test_1017502-bookmarks_foreign_count.js] +[test_384228.js] +[test_385829.js] +[test_388695.js] +[test_393498.js] +[test_405938_restore_queries.js] +[test_424958-json-quoted-folders.js] +[test_448584.js] +[test_458683.js] +[test_466303-json-remove-backups.js] +[test_477583_json-backup-in-future.js] +[test_818584-discard-duplicate-backups.js] +[test_818587_compress-bookmarks-backups.js] +[test_818593-store-backup-metadata.js] +[test_992901-backup-unsorted-hierarchy.js] +[test_997030-bookmarks-html-encode.js] +[test_1129529.js] +support-files = + bookmarks_long_tag.json +[test_async_observers.js] +[test_bmindex.js] +[test_bookmarkstree_cache.js] +[test_bookmarks_eraseEverything.js] +[test_bookmarks_fetch.js] +[test_bookmarks_getRecent.js] +[test_bookmarks_insert.js] +[test_bookmarks_insertTree.js] +[test_bookmarks_notifications.js] +[test_bookmarks_moveToFolder.js] +[test_bookmarks_remove.js] +[test_bookmarks_remove_batch.js] +[test_bookmarks_reorder.js] +[test_bookmarks_search.js] +[test_bookmarks_update.js] +[test_insertTree_fixupOrSkipInvalidEntries.js] +[test_keywords.js] +[test_nsINavBookmarkObserver.js] +[test_removeFolderTransaction_reinsert.js] +[test_savedsearches.js] +[test_sync_fields.js] +[test_tags.js] +[test_untitled.js] |