summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/bookmarks
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 01:47:29 +0000
commit0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d (patch)
treea31f07c9bcca9d56ce61e9a1ffd30ef350d513aa /toolkit/components/places/tests/bookmarks
parentInitial commit. (diff)
downloadfirefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.tar.xz
firefox-esr-0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d.zip
Adding upstream version 115.8.0esr.upstream/115.8.0esr
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/places/tests/bookmarks')
-rw-r--r--toolkit/components/places/tests/bookmarks/bookmarks_long_tag.json55
-rw-r--r--toolkit/components/places/tests/bookmarks/head_bookmarks.js150
-rw-r--r--toolkit/components/places/tests/bookmarks/test_1016953-renaming-uncompressed.js119
-rw-r--r--toolkit/components/places/tests/bookmarks/test_1017502-bookmarks_foreign_count.js117
-rw-r--r--toolkit/components/places/tests/bookmarks/test_1129529.js24
-rw-r--r--toolkit/components/places/tests/bookmarks/test_384228.js93
-rw-r--r--toolkit/components/places/tests/bookmarks/test_385829.js180
-rw-r--r--toolkit/components/places/tests/bookmarks/test_388695.js45
-rw-r--r--toolkit/components/places/tests/bookmarks/test_393498.js161
-rw-r--r--toolkit/components/places/tests/bookmarks/test_405938_restore_queries.js253
-rw-r--r--toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js48
-rw-r--r--toolkit/components/places/tests/bookmarks/test_448584.js90
-rw-r--r--toolkit/components/places/tests/bookmarks/test_458683.js111
-rw-r--r--toolkit/components/places/tests/bookmarks/test_466303-json-remove-backups.js86
-rw-r--r--toolkit/components/places/tests/bookmarks/test_477583_json-backup-in-future.js56
-rw-r--r--toolkit/components/places/tests/bookmarks/test_818584-discard-duplicate-backups.js66
-rw-r--r--toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js61
-rw-r--r--toolkit/components/places/tests/bookmarks/test_818593-store-backup-metadata.js53
-rw-r--r--toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js65
-rw-r--r--toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js37
-rw-r--r--toolkit/components/places/tests/bookmarks/test_async_observers.js71
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bmindex.js133
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmark_observer.js937
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js207
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js614
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_getRecent.js117
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js432
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js590
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_moveToFolder.js723
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js1135
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js465
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_remove_batch.js129
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_reorder.js310
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_search.js339
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarks_update.js587
-rw-r--r--toolkit/components/places/tests/bookmarks/test_bookmarkstree_cache.js23
-rw-r--r--toolkit/components/places/tests/bookmarks/test_insertTree_fixupOrSkipInvalidEntries.js114
-rw-r--r--toolkit/components/places/tests/bookmarks/test_keywords.js691
-rw-r--r--toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js115
-rw-r--r--toolkit/components/places/tests/bookmarks/test_savedsearches.js224
-rw-r--r--toolkit/components/places/tests/bookmarks/test_sync_fields.js438
-rw-r--r--toolkit/components/places/tests/bookmarks/test_tags.js128
-rw-r--r--toolkit/components/places/tests/bookmarks/test_untitled.js114
-rw-r--r--toolkit/components/places/tests/bookmarks/xpcshell.ini48
44 files changed, 10554 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..25fef61eb2
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/bookmarks_long_tag.json
@@ -0,0 +1,55 @@
+{
+ "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..e306b4080d
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/head_bookmarks.js
@@ -0,0 +1,150 @@
+/* -*- 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 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..8fa4731823
--- /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..ab7cf57f61
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_393498.js
@@ -0,0 +1,161 @@
+/* -*- 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;
+ }
+ }
+ }
+ },
+};
+
+observer.handlePlacesEvents = observer.handlePlacesEvents.bind(observer);
+PlacesUtils.observers.addListener(
+ ["bookmark-added", "bookmark-time-changed", "bookmark-title-changed"],
+ observer.handlePlacesEvents
+);
+
+registerCleanupFunction(function () {
+ 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..c893f1db3f
--- /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..2a7ce3f003
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_424958-json-quoted-folders.js
@@ -0,0 +1,48 @@
+/* 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..07665327b1
--- /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..d31fca66e1
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_458683.js
@@ -0,0 +1,111 @@
+/* -*- 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..be5d53f8c6
--- /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..91e0c50f7e
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_818587_compress-bookmarks-backups.js
@@ -0,0 +1,61 @@
+/* -*- 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..c835a3bd09
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_992901-backup-unsorted-hierarchy.js
@@ -0,0 +1,65 @@
+/* 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..6f3132b275
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_997030-bookmarks-html-encode.js
@@ -0,0 +1,37 @@
+/* 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..504f489339
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_async_observers.js
@@ -0,0 +1,71 @@
+/* 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")
+ )
+ );
+
+ 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(
+ "page-removed",
+ events =>
+ events.some(
+ event =>
+ event.url === "http://book.ma.rk/" &&
+ !event.isRemovedFromStore &&
+ !event.isPartialVisistsRemoval
+ )
+ );
+ 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_bookmark_observer.js b/toolkit/components/places/tests/bookmarks/test_bookmark_observer.js
new file mode 100644
index 0000000000..9ad01cfab9
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmark_observer.js
@@ -0,0 +1,937 @@
+/* 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;
+ },
+
+ 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 = {
+ 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 = bm.url.URI;
+ 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 = bm.url.URI;
+ 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_bookmarks_eraseEverything.js b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
new file mode 100644
index 0000000000..55bcea5978
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_eraseEverything.js
@@ -0,0 +1,207 @@
+/* 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 = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://example.com/" }
+ );
+ let frecencyForMozilla = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: "http://mozilla.org/" }
+ );
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.ok(
+ (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: "http://example.com/",
+ })) > frecencyForExample
+ );
+ Assert.ok(
+ (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: "http://example.com/",
+ })) > frecencyForMozilla
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: "http://example.com/",
+ }),
+ frecencyForExample
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: "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..d04d3ee5da
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_fetch.js
@@ -0,0 +1,614 @@
+/* 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");
+ Assert.strictEqual(bm2.childCount, undefined);
+
+ 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);
+ Assert.deepEqual(bm1.dateAdded, bm1.lastModified);
+
+ // Inserting a child updates both the childCount and lastModified of bm1,
+ // though the bm1 object is static once fetched, thus later we'll manually
+ // update it.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: bm1.guid,
+ url: "https://www.mozilla.org/",
+ title: "",
+ });
+
+ let bm2 = await PlacesUtils.bookmarks.fetch(bm1.guid);
+ checkBookmarkObject(bm2);
+
+ Assert.equal(bm2.childCount, 1);
+ bm1.childCount = bm2.childCount;
+ bm1.lastModified = bm2.lastModified;
+
+ Assert.deepEqual(bm1, bm2);
+ Assert.equal(bm2.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ Assert.equal(bm2.index, 0);
+ 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.equal(bm2.childCount, 0);
+ // Insert doesn't populate childCount (it would always be 0 anyway), so set
+ // it to be able to just use deepEqual.
+ bm1.childCount = bm2.childCount;
+
+ 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",
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: bm3.guid,
+ url: "https://www.mozilla.org/",
+ title: "",
+ });
+ 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.equal(gAccumulator.results[0].childCount, 1);
+ bm3.childCount = gAccumulator.results[0].childCount;
+ bm3.lastModified = gAccumulator.results[0].lastModified;
+ Assert.deepEqual(bm3, gAccumulator.results[0]);
+
+ Assert.equal(bm3.childCount, 1);
+
+ 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(bm1.url.URI, ["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(bm1.url.URI, ["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,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "sub folder",
+ index: 0,
+ });
+ checkBookmarkObject(bm2);
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: bm3.guid,
+ url: "http://mozilla.org/",
+ title: "sub bookmark",
+ });
+
+ await PlacesUtils.bookmarks.fetch(
+ { parentGuid: folder1.guid },
+ gAccumulator.callback
+ );
+
+ Assert.equal(gAccumulator.results.length, 3);
+
+ Assert.equal(gAccumulator.results[0].childCount, 1);
+ bm3.childCount = gAccumulator.results[0].childCount;
+ bm3.lastModified = gAccumulator.results[0].lastModified;
+ Assert.deepEqual(bm3, gAccumulator.results[0]);
+ 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..df99a5fe3d
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insert.js
@@ -0,0 +1,432 @@
+/* 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({ dateAdded: new Date(NaN) }),
+ /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'/
+ );
+ Assert.throws(
+ () => PlacesUtils.bookmarks.insert({ lastModified: new Date(NaN) }),
+ /Invalid value for property 'lastModified'/
+ );
+
+ let past = new Date(Date.now() - 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.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+ );
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.greater(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: 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..0fad7d4b31
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_insertTree.js
@@ -0,0 +1,590 @@
+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 = [{ dateAdded: new Date(NaN) }];
+ 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 = Date.now();
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: 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..08afb6001d
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_notifications.js
@@ -0,0 +1,1135 @@
+/* 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());
+ // Ensure there's at least one item out of place, since random does not
+ // necessarily mean they are unordered.
+ if (sorted[0].url == bookmarks[0].url) {
+ sorted.push(sorted.shift());
+ }
+
+ 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..01bb591e3c
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove.js
@@ -0,0 +1,465 @@
+/* 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");
+}
+
+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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ await promise;
+
+ // This second one checks the frecency is changed when we remove the bookmark.
+ promise = promiseRankingChanged();
+
+ await PlacesUtils.bookmarks.remove(bm1.guid);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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;
+ }
+ );
+ 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;
+ }
+ );
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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 PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ 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..51a2a4dc2b
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_remove_batch.js
@@ -0,0 +1,129 @@
+/* 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");
+ 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;
+ }
+ );
+ 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..2b34b2c8f9
--- /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(bm1.url.URI, ["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..1c9bead831
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_bookmarks_update.js
@@ -0,0 +1,587 @@
+/* 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;
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let frecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url: 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);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: "http://example.com/",
+ }),
+ frecency,
+ "Check frecency for example.com"
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: "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..9a676e0ed0
--- /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..fca9ebf32a
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_keywords.js
@@ -0,0 +1,691 @@
+/* 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() {
+ const observer = {
+ notifications: [],
+ _start() {
+ this._handle = this._handle.bind(this);
+ PlacesUtils.observers.addListener(
+ ["bookmark-keyword-changed"],
+ this._handle
+ );
+ },
+ _handle(events) {
+ for (const event of events) {
+ this.notifications.push({
+ type: event.type,
+ id: event.id,
+ itemType: event.itemType,
+ url: event.url,
+ guid: event.guid,
+ parentGuid: event.parentGuid,
+ keyword: event.keyword,
+ lastModified: new Date(event.lastModified),
+ source: event.source,
+ isTagging: event.isTagging,
+ });
+ }
+ },
+ check(expected) {
+ PlacesUtils.observers.removeListener(
+ ["bookmark-keyword-changed"],
+ this._handle
+ );
+ Assert.deepEqual(this.notifications, expected);
+ },
+ };
+ observer._start();
+ return observer;
+}
+
+add_task(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([
+ {
+ type: "bookmark-keyword-changed",
+ id: itemId,
+ itemType: bookmark.type,
+ url: bookmark.url,
+ guid: bookmark.guid,
+ parentGuid: bookmark.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+ await check_keyword(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([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(bookmark1.guid),
+ itemType: bookmark1.type,
+ url: bookmark1.url,
+ guid: bookmark1.guid,
+ parentGuid: bookmark1.parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmark1.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(bookmark2.guid),
+ itemType: bookmark2.type,
+ url: bookmark2.url,
+ guid: bookmark2.guid,
+ parentGuid: bookmark2.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark2.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ // 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([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(bookmarks[0].guid),
+ itemType: bookmarks[0].type,
+ url: bookmarks[0].url,
+ guid: bookmarks[0].guid,
+ parentGuid: bookmarks[0].parentGuid,
+ keyword: "keyword2",
+ lastModified: new Date(bookmarks[0].lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(bookmarks[1].guid),
+ itemType: bookmarks[1].type,
+ url: bookmarks[1].url,
+ guid: bookmarks[1].guid,
+ parentGuid: bookmarks[1].parentGuid,
+ keyword: "keyword2",
+ lastModified: new Date(bookmarks[1].lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+ 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([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(bookmarks[0].guid),
+ itemType: bookmarks[0].type,
+ url: bookmarks[0].url,
+ guid: bookmarks[0].guid,
+ parentGuid: bookmarks[0].parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmarks[0].lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(bookmarks[1].guid),
+ itemType: bookmarks[1].type,
+ url: bookmarks[1].url,
+ guid: bookmarks[1].guid,
+ parentGuid: bookmarks[1].parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmarks[1].lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(bookmarks[2].guid),
+ itemType: bookmarks[2].type,
+ url: bookmarks[2].url,
+ guid: bookmarks[2].guid,
+ parentGuid: bookmarks[2].parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmarks[2].lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ 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([
+ {
+ type: "bookmark-keyword-changed",
+ id: itemId,
+ itemType: bookmark.type,
+ url: bookmark.url,
+ guid: bookmark.guid,
+ parentGuid: bookmark.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ await check_keyword(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([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(oldBmk.guid),
+ itemType: oldBmk.type,
+ url: oldBmk.url,
+ guid: oldBmk.guid,
+ parentGuid: oldBmk.parentGuid,
+ keyword: "",
+ lastModified: new Date(oldBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(newBmk.guid),
+ itemType: newBmk.type,
+ url: newBmk.url,
+ guid: newBmk.guid,
+ parentGuid: newBmk.parentGuid,
+ keyword: "",
+ lastModified: new Date(newBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(newBmk.guid),
+ itemType: newBmk.type,
+ url: newBmk.url,
+ guid: newBmk.guid,
+ parentGuid: newBmk.parentGuid,
+ keyword: "kw1-1",
+ lastModified: new Date(newBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(newBmk.guid),
+ itemType: newBmk.type,
+ url: newBmk.url,
+ guid: newBmk.guid,
+ parentGuid: newBmk.parentGuid,
+ keyword: "kw1-2",
+ lastModified: new Date(newBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ 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([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(oldBmk.guid),
+ itemType: oldBmk.type,
+ url: oldBmk.url,
+ guid: oldBmk.guid,
+ parentGuid: oldBmk.parentGuid,
+ keyword: "",
+ lastModified: new Date(oldBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(newBmk.guid),
+ itemType: newBmk.type,
+ url: newBmk.url,
+ guid: newBmk.guid,
+ parentGuid: newBmk.parentGuid,
+ keyword: "",
+ lastModified: new Date(newBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(newBmk.guid),
+ itemType: newBmk.type,
+ url: newBmk.url,
+ guid: newBmk.guid,
+ parentGuid: newBmk.parentGuid,
+ keyword: "kw8-1",
+ lastModified: new Date(newBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesUtils.promiseItemId(newBmk.guid),
+ itemType: newBmk.type,
+ url: newBmk.url,
+ guid: newBmk.guid,
+ parentGuid: newBmk.parentGuid,
+ keyword: "kw8-2",
+ lastModified: new Date(newBmk.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ 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(
+ "bookmark-keyword-changed",
+ events =>
+ events.some(event => event.guid === fx.guid && event.keyword === "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_removeFolderTransaction_reinsert.js b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js
new file mode 100644
index 0000000000..1c9e55849d
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_removeFolderTransaction_reinsert.js
@@ -0,0 +1,115 @@
+/* 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,
+ ]);
+ }
+ };
+ PlacesUtils.observers.addListener(
+ ["bookmark-added", "bookmark-removed"],
+ listener
+ );
+ PlacesUtils.registerShutdownFunction(function () {
+ 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..3334d09b7c
--- /dev/null
+++ b/toolkit/components/places/tests/bookmarks/test_tags.js
@@ -0,0 +1,128 @@
+/* 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(bm.url.URI, ["1", "2"]);
+ tags = await PlacesUtils.bookmarks.fetchTags();
+ Assert.deepEqual(tags, [
+ { name: "1", count: 1 },
+ { name: "2", count: 1 },
+ ]);
+
+ PlacesUtils.tagging.untagURI(bm.url.URI, ["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(bm2.url.URI, ["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(bm1.url.URI, ["egg", "ratafià"]);
+ let bm2 = await PlacesUtils.bookmarks.insert({
+ url: "http://mushroom.org/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ PlacesUtils.tagging.tagURI(bm2.url.URI, ["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..91d3741bb4
--- /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_bookmark_observer.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_removeFolderTransaction_reinsert.js]
+[test_savedsearches.js]
+[test_sync_fields.js]
+[test_tags.js]
+[test_untitled.js]