summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/unit
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/places/tests/unit
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/places/tests/unit')
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.corrupt.html36
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.json307
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.preplaces.html36
-rw-r--r--toolkit/components/places/tests/unit/bookmarks_corrupt.json72
-rw-r--r--toolkit/components/places/tests/unit/bookmarks_html_localized.html21
-rw-r--r--toolkit/components/places/tests/unit/bookmarks_html_singleframe.html10
-rw-r--r--toolkit/components/places/tests/unit/bookmarks_iconuri.json307
-rw-r--r--toolkit/components/places/tests/unit/head_bookmarks.js30
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json135
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json101
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json159
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json89
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json89
-rw-r--r--toolkit/components/places/tests/unit/test_1085291.js48
-rw-r--r--toolkit/components/places/tests/unit/test_1105208.js25
-rw-r--r--toolkit/components/places/tests/unit/test_1105866.js77
-rw-r--r--toolkit/components/places/tests/unit/test_1606731.js21
-rw-r--r--toolkit/components/places/tests/unit/test_331487.js113
-rw-r--r--toolkit/components/places/tests/unit/test_384370.js188
-rw-r--r--toolkit/components/places/tests/unit/test_385397.js152
-rw-r--r--toolkit/components/places/tests/unit/test_399266.js82
-rw-r--r--toolkit/components/places/tests/unit/test_402799.js60
-rw-r--r--toolkit/components/places/tests/unit/test_412132.js181
-rw-r--r--toolkit/components/places/tests/unit/test_415460.js37
-rw-r--r--toolkit/components/places/tests/unit/test_415757.js92
-rw-r--r--toolkit/components/places/tests/unit/test_419792_node_tags_property.js52
-rw-r--r--toolkit/components/places/tests/unit/test_425563.js76
-rw-r--r--toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js45
-rw-r--r--toolkit/components/places/tests/unit/test_433317_query_title_update.js43
-rw-r--r--toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js52
-rw-r--r--toolkit/components/places/tests/unit/test_454977.js121
-rw-r--r--toolkit/components/places/tests/unit/test_463863.js56
-rw-r--r--toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js19
-rw-r--r--toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js130
-rw-r--r--toolkit/components/places/tests/unit/test_536081.js35
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesDBUtils_removeOldCorruptDBs.js42
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesQuery_history.js245
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_isRootItem.js21
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_unwrapNodes_place.js34
-rw-r--r--toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js84
-rw-r--r--toolkit/components/places/tests/unit/test_async_transactions.js2125
-rw-r--r--toolkit/components/places/tests/unit/test_autocomplete_match_fallbackTitle.js27
-rw-r--r--toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js56
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html.js417
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js126
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_escape_entities.js98
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js64
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_localized.js51
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js31
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_json.js368
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js65
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js319
-rw-r--r--toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js78
-rw-r--r--toolkit/components/places/tests/unit/test_browserhistory.js122
-rw-r--r--toolkit/components/places/tests/unit/test_childlessTags.js140
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_decay.js82
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_observers.js99
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_origins_alternative.js301
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_origins_recalc.js109
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_pages_alternative.js364
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_pages_recalc_alt.js97
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_recalc_triggers.js281
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_recalculator.js178
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_unvisited_bookmark.js54
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_zero_updated.js38
-rw-r--r--toolkit/components/places/tests/unit/test_getChildIndex.js73
-rw-r--r--toolkit/components/places/tests/unit/test_get_query_param_sql_function.js21
-rw-r--r--toolkit/components/places/tests/unit/test_hash.js50
-rw-r--r--toolkit/components/places/tests/unit/test_history.js158
-rw-r--r--toolkit/components/places/tests/unit/test_history_clear.js146
-rw-r--r--toolkit/components/places/tests/unit/test_history_notifications.js50
-rw-r--r--toolkit/components/places/tests/unit/test_history_observer.js186
-rw-r--r--toolkit/components/places/tests/unit/test_history_sidebar.js418
-rw-r--r--toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js326
-rw-r--r--toolkit/components/places/tests/unit/test_isPageInDB.js10
-rw-r--r--toolkit/components/places/tests/unit/test_isURIVisited.js73
-rw-r--r--toolkit/components/places/tests/unit/test_isvisited.js69
-rw-r--r--toolkit/components/places/tests/unit/test_keywords.js733
-rw-r--r--toolkit/components/places/tests/unit/test_lastModified.js78
-rw-r--r--toolkit/components/places/tests/unit/test_markpageas.js48
-rw-r--r--toolkit/components/places/tests/unit/test_metadata.js285
-rw-r--r--toolkit/components/places/tests/unit/test_missing_builtin_folders.js112
-rw-r--r--toolkit/components/places/tests/unit/test_missing_root_folder.js106
-rw-r--r--toolkit/components/places/tests/unit/test_multi_observation.js384
-rw-r--r--toolkit/components/places/tests/unit/test_multi_word_tags.js147
-rw-r--r--toolkit/components/places/tests/unit/test_nested_notifications.js186
-rw-r--r--toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js284
-rw-r--r--toolkit/components/places/tests/unit/test_null_interfaces.js105
-rw-r--r--toolkit/components/places/tests/unit/test_origins.js1113
-rw-r--r--toolkit/components/places/tests/unit/test_origins_parsing.js104
-rw-r--r--toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js256
-rw-r--r--toolkit/components/places/tests/unit/test_placeURIs.js18
-rw-r--r--toolkit/components/places/tests/unit/test_promiseBookmarksTree.js282
-rw-r--r--toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js39
-rw-r--r--toolkit/components/places/tests/unit/test_result_sort.js112
-rw-r--r--toolkit/components/places/tests/unit/test_resultsAsVisit_details.js100
-rw-r--r--toolkit/components/places/tests/unit/test_sql_function_origin.js79
-rw-r--r--toolkit/components/places/tests/unit/test_sql_guid_functions.js94
-rw-r--r--toolkit/components/places/tests/unit/test_tag_autocomplete_search.js119
-rw-r--r--toolkit/components/places/tests/unit/test_tagging.js188
-rw-r--r--toolkit/components/places/tests/unit/test_telemetry.js151
-rw-r--r--toolkit/components/places/tests/unit/test_update_frecency_after_delete.js211
-rw-r--r--toolkit/components/places/tests/unit/test_utils_backups_create.js152
-rw-r--r--toolkit/components/places/tests/unit/test_utils_backups_hasRecent.js43
-rw-r--r--toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js266
-rw-r--r--toolkit/components/places/tests/unit/test_utils_timeConversion.js51
-rw-r--r--toolkit/components/places/tests/unit/test_visitsInDB.js12
-rw-r--r--toolkit/components/places/tests/unit/xpcshell.toml210
108 files changed, 16861 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/unit/bookmarks.corrupt.html b/toolkit/components/places/tests/unit/bookmarks.corrupt.html
new file mode 100644
index 0000000000..3cf43367fb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.corrupt.html
@@ -0,0 +1,36 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<TITLE>Bookmarks</TITLE>
+<H1 LAST_MODIFIED="1177541029">Bookmarks</H1>
+
+<DL><p>
+ <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3>
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
+ <DT><A HREF="b0rked" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
+ </DL><p>
+ <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3>
+<DD>folder test comment
+ <DL><p>
+ <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&amp;text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A>
+<DD>item description
+ </DL>
+ <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
+ <DL><p>
+ <DT><A HREF="http://example.tld">Example.tld</A>
+ </DL><p>
+ <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A>
+ <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A>
+ <DT><A HREF="http://bogus-icon.mozilla.com/" ICON="b0rked" ID="rdf:#$GvPhC3">Getting Started</A>
+<DD>Livemark test comment
+ </DL><p>
+</DL><p>
diff --git a/toolkit/components/places/tests/unit/bookmarks.json b/toolkit/components/places/tests/unit/bookmarks.json
new file mode 100644
index 0000000000..27ed9ce5ca
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.json
@@ -0,0 +1,307 @@
+{
+ "guid": "root________",
+ "title": "",
+ "id": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551978957783,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "id": 2,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551979382837,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FF",
+ "title": "Mozilla Firefox",
+ "id": 6,
+ "parent": 2,
+ "dateAdded": 1361551979350273,
+ "lastModified": 1361551979376699,
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FG",
+ "title": "Help and Tutorials",
+ "id": 7,
+ "parent": 6,
+ "dateAdded": 1361551979356436,
+ "lastModified": 1361551979362718,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/help/",
+ "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FH",
+ "index": 1,
+ "title": "Customize Firefox",
+ "id": 8,
+ "parent": 6,
+ "dateAdded": 1361551979365662,
+ "lastModified": 1361551979368077,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/",
+ "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FJ",
+ "index": 3,
+ "title": "About Us",
+ "id": 10,
+ "parent": 6,
+ "dateAdded": 1361551979376699,
+ "lastModified": 1361551979379060,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/about/",
+ "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FI",
+ "index": 2,
+ "title": "Get Involved",
+ "id": 9,
+ "parent": 6,
+ "dateAdded": 1361551979371071,
+ "lastModified": 1361551979373745,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/community/",
+ "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "QFM-QnE2ZpMz",
+ "title": "Test null postData",
+ "index": 4,
+ "dateAdded": 1481639510868000,
+ "lastModified": 1489563704300000,
+ "id": 17,
+ "charset": "UTF-8",
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "value": "The best"
+ }
+ ],
+ "type": "text/x-moz-place",
+ "uri": "http://example.com/search?q=%s&suggid=",
+ "postData": null
+ }
+ ]
+ },
+ {
+ "guid": "OCyeUO5uu9FK",
+ "index": 1,
+ "title": "",
+ "id": 11,
+ "parent": 2,
+ "dateAdded": 1361551979380988,
+ "lastModified": 1361551979380988,
+ "type": "text/x-moz-place-separator"
+ },
+ {
+ "guid": "OCyeUO5uu9FL",
+ "index": 2,
+ "title": "test",
+ "id": 12,
+ "parent": 2,
+ "dateAdded": 1177541020000000,
+ "lastModified": 1177541050000000,
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "folder test comment"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9GX",
+ "title": "test post keyword",
+ "id": 13,
+ "parent": 12,
+ "dateAdded": 1177375336000000,
+ "lastModified": 1177375423000000,
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "item description"
+ },
+ {
+ "name": "bookmarkProperties/loadInSidebar",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 1,
+ "value": 1
+ }
+ ],
+ "type": "text/x-moz-place",
+ "uri": "http://test/post",
+ "keyword": "test",
+ "charset": "ISO-8859-1",
+ "postData": "hidden1%3Dbar&text1%3D%25s"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "index": 1,
+ "title": "Bookmarks Toolbar",
+ "id": 3,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1177541050000000,
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FB",
+ "title": "Getting Started",
+ "id": 15,
+ "parent": 3,
+ "dateAdded": 1361551979409695,
+ "lastModified": 1361551979412080,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/central/",
+ "icon": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FR",
+ "index": 1,
+ "title": "Latest Headlines",
+ "id": 16,
+ "parent": 3,
+ "dateAdded": 1361551979451584,
+ "lastModified": 1361551979457086,
+ "livemark": 1,
+ "annos": [
+ {
+ "name": "livemark/feedURI",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
+ },
+ {
+ "name": "livemark/siteURI",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": []
+ }
+ ]
+ },
+ {
+ "index": 2,
+ "title": "Tags",
+ "id": 4,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551978957783,
+ "type": "text/x-moz-place-container",
+ "root": "tagsFolder",
+ "children": []
+ },
+ {
+ "index": 3,
+ "title": "Unsorted Bookmarks",
+ "id": 5,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1177541050000000,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FW",
+ "title": "Example.tld",
+ "id": 14,
+ "parent": 5,
+ "dateAdded": 1361551979401846,
+ "lastModified": 1361551979402952,
+ "type": "text/x-moz-place",
+ "uri": "http://example.tld/"
+ },
+ {
+ "guid": "Cfkety492Afk",
+ "title": "test tagged bookmark",
+ "id": 15,
+ "parent": 5,
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "type": "text/x-moz-place",
+ "uri": "http://example.tld/tagged",
+ "tags": "foo"
+ },
+ {
+ "guid": "lOZGoFR1eXbl",
+ "title": "Bookmarks Toolbar Shortcut",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 16,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=TOOLBAR"
+ },
+ {
+ "guid": "7yJWnBVhjRtP",
+ "title": "Folder Shortcut",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 17,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=6"
+ },
+ {
+ "guid": "vm5QXWuWc12l",
+ "title": "Folder Shortcut 2",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 18,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=6123443"
+ },
+ {
+ "guid": "Icg1XlIozA1D",
+ "title": "Folder Shortcut 3",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 18,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=6&folder=BOOKMARKS_MENU"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/bookmarks.preplaces.html b/toolkit/components/places/tests/unit/bookmarks.preplaces.html
new file mode 100644
index 0000000000..0ddf7725b4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.preplaces.html
@@ -0,0 +1,36 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<TITLE>Bookmarks</TITLE>
+<H1 LAST_MODIFIED="1177541029">Bookmarks</H1>
+
+<DL><p>
+ <DT><H3 ID="rdf:#$ZvPhC3">Mozilla Firefox</H3>
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1">Help and Tutorials</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/customize/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$32iCK1">Customize Firefox</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/community/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$42iCK1">Get Involved</A>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/about/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$52iCK1">About Us</A>
+ </DL><p>
+ <HR>
+ <DT><H3 ADD_DATE="1177541020" LAST_MODIFIED="1177541050" ID="rdf:#$74Gpx2">test</H3>
+<DD>folder test comment
+ <DL><p>
+ <DT><A HREF="http://test/post" ADD_DATE="1177375336" LAST_MODIFIED="1177375423" SHORTCUTURL="test" WEB_PANEL="true" POST_DATA="hidden1%3Dbar&amp;text1%3D%25s" LAST_CHARSET="ISO-8859-1" ID="rdf:#$pYFe7">test post keyword</A>
+<DD>item description
+ </DL>
+ <DT><H3 UNFILED_BOOKMARKS_FOLDER="true">Unsorted Bookmarks</H3>
+ <DL><p>
+ <DT><A HREF="http://example.tld">Example.tld</A>
+ </DL><p>
+ <DT><H3 LAST_MODIFIED="1177541040" PERSONAL_TOOLBAR_FOLDER="true" ID="rdf:#$FvPhC3">Bookmarks Toolbar Folder</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://en-US.www.mozilla.com/en-US/firefox/central/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$GvPhC3">Getting Started</A>
+ <DT><A HREF="http://en-US.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/" LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines</A>
+ <DT><A LAST_MODIFIED="1177541035" FEEDURL="http://en-US.fxfeeds.mozilla.com/en-US/firefox/headlines.xml" ID="rdf:#$HvPhC3">Latest Headlines No Site</A>
+<DD>Livemark test comment
+ </DL><p>
+</DL><p>
diff --git a/toolkit/components/places/tests/unit/bookmarks_corrupt.json b/toolkit/components/places/tests/unit/bookmarks_corrupt.json
new file mode 100644
index 0000000000..93f21d3ece
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks_corrupt.json
@@ -0,0 +1,72 @@
+{
+ "guid": "root________",
+ "title": "",
+ "id": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551978957783,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "id": 2,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551979382837,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FG",
+ "title": "Help and Tutorials",
+ "id": 7,
+ "dateAdded": 1361551979356436,
+ "lastModified": 1361551979362718,
+ "type": "x/invalid",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/help/"
+ },
+ {
+ "guid": "OCyeUO5uu9FH",
+ "index": 1,
+ "title": "Customize Firefox",
+ "id": 8,
+ "dateAdded": 1361551979365662,
+ "lastModified": 1361551979368077,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/"
+ },
+ {
+ "guid": "OCyeUO5uu9FG",
+ "title": "Bad URL",
+ "id": 9,
+ "dateAdded": 1361551979356436,
+ "lastModified": 1361551979362718,
+ "type": "text/x-moz-place",
+ "uri": "http:///"
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "Bookmarks Toolbar",
+ "id": 2,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551979382837,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FG",
+ "title": "Bad URL",
+ "id": 9,
+ "dateAdded": 1361551979356436,
+ "lastModified": 1361551979362718,
+ "type": "text/x-moz-place",
+ "uri": "http:///"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/bookmarks_html_localized.html b/toolkit/components/places/tests/unit/bookmarks_html_localized.html
new file mode 100644
index 0000000000..bc3bacc54d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks_html_localized.html
@@ -0,0 +1,21 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+<!-- This is an automatically generated file.
+ It will be read and overwritten.
+ DO NOT EDIT! -->
+<HTML>
+<HEAD>
+<META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+<TITLE>Bookmarks</TITLE>
+<LINK REL="localization" HREF="bookmarks_html_localized.ftl">
+</HEAD>
+<BODY>
+<H1 LAST_MODIFIED="1177541029">Bookmarks</H1>
+
+<DL><p>
+ <DT><H3 ID="rdf:#$ZvPhC3" data-l10n-id="bookmarks-html-localized-folder">bookmarks-html-localized-folder</H3>
+ <DL><p>
+ <DT><A HREF="http://www.mozilla.com/firefox/help/" ICON="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==" ID="rdf:#$22iCK1" data-l10n-id="bookmarks-html-localized-bookmark">bookmarks-html-localized-bookmark</A>
+ </DL><p>
+</DL><p>
+</BODY>
+</HTML>
diff --git a/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html
new file mode 100644
index 0000000000..9fe662f320
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks_html_singleframe.html
@@ -0,0 +1,10 @@
+<!DOCTYPE NETSCAPE-Bookmark-file-1>
+ <HTML>
+ <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=UTF-8">
+ <Title>Bookmarks</Title>
+ <H1>Bookmarks</H1>
+ <DT><H3>Subtitle</H3>
+ <DL><p>
+ <DT><A HREF="http://www.mozilla.org/">Mozilla</A>
+ </DL><p>
+</HTML>
diff --git a/toolkit/components/places/tests/unit/bookmarks_iconuri.json b/toolkit/components/places/tests/unit/bookmarks_iconuri.json
new file mode 100644
index 0000000000..4059c1d53f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks_iconuri.json
@@ -0,0 +1,307 @@
+{
+ "guid": "root________",
+ "title": "",
+ "id": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551978957783,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "id": 2,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551979382837,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FF",
+ "title": "Mozilla Firefox",
+ "id": 6,
+ "parent": 2,
+ "dateAdded": 1361551979350273,
+ "lastModified": 1361551979376699,
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FG",
+ "title": "Help and Tutorials",
+ "id": 7,
+ "parent": 6,
+ "dateAdded": 1361551979356436,
+ "lastModified": 1361551979362718,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/help/",
+ "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FH",
+ "index": 1,
+ "title": "Customize Firefox",
+ "id": 8,
+ "parent": 6,
+ "dateAdded": 1361551979365662,
+ "lastModified": 1361551979368077,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/customize/",
+ "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FJ",
+ "index": 3,
+ "title": "About Us",
+ "id": 10,
+ "parent": 6,
+ "dateAdded": 1361551979376699,
+ "lastModified": 1361551979379060,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/about/",
+ "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FI",
+ "index": 2,
+ "title": "Get Involved",
+ "id": 9,
+ "parent": 6,
+ "dateAdded": 1361551979371071,
+ "lastModified": 1361551979373745,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/community/",
+ "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "QFM-QnE2ZpMz",
+ "title": "Test null postData",
+ "index": 4,
+ "dateAdded": 1481639510868000,
+ "lastModified": 1489563704300000,
+ "id": 17,
+ "charset": "UTF-8",
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "value": "The best"
+ }
+ ],
+ "type": "text/x-moz-place",
+ "uri": "http://example.com/search?q=%s&suggid=",
+ "postData": null
+ }
+ ]
+ },
+ {
+ "guid": "OCyeUO5uu9FK",
+ "index": 1,
+ "title": "",
+ "id": 11,
+ "parent": 2,
+ "dateAdded": 1361551979380988,
+ "lastModified": 1361551979380988,
+ "type": "text/x-moz-place-separator"
+ },
+ {
+ "guid": "OCyeUO5uu9FL",
+ "index": 2,
+ "title": "test",
+ "id": 12,
+ "parent": 2,
+ "dateAdded": 1177541020000000,
+ "lastModified": 1177541050000000,
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "folder test comment"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9GX",
+ "title": "test post keyword",
+ "id": 13,
+ "parent": 12,
+ "dateAdded": 1177375336000000,
+ "lastModified": 1177375423000000,
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "item description"
+ },
+ {
+ "name": "bookmarkProperties/loadInSidebar",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 1,
+ "value": 1
+ }
+ ],
+ "type": "text/x-moz-place",
+ "uri": "http://test/post",
+ "keyword": "test",
+ "charset": "ISO-8859-1",
+ "postData": "hidden1%3Dbar&text1%3D%25s"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "index": 1,
+ "title": "Bookmarks Toolbar",
+ "id": 3,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1177541050000000,
+ "annos": [
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FB",
+ "title": "Getting Started",
+ "id": 15,
+ "parent": 3,
+ "dateAdded": 1361551979409695,
+ "lastModified": 1361551979412080,
+ "type": "text/x-moz-place",
+ "uri": "http://en-us.www.mozilla.com/en-US/firefox/central/",
+ "iconUri": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg=="
+ },
+ {
+ "guid": "OCyeUO5uu9FR",
+ "index": 1,
+ "title": "Latest Headlines",
+ "id": 16,
+ "parent": 3,
+ "dateAdded": 1361551979451584,
+ "lastModified": 1361551979457086,
+ "livemark": 1,
+ "annos": [
+ {
+ "name": "livemark/feedURI",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
+ },
+ {
+ "name": "livemark/siteURI",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": []
+ }
+ ]
+ },
+ {
+ "index": 2,
+ "title": "Tags",
+ "id": 4,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1361551978957783,
+ "type": "text/x-moz-place-container",
+ "root": "tagsFolder",
+ "children": []
+ },
+ {
+ "index": 3,
+ "title": "Unsorted Bookmarks",
+ "id": 5,
+ "parent": 1,
+ "dateAdded": 1361551978957783,
+ "lastModified": 1177541050000000,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder",
+ "children": [
+ {
+ "guid": "OCyeUO5uu9FW",
+ "title": "Example.tld",
+ "id": 14,
+ "parent": 5,
+ "dateAdded": 1361551979401846,
+ "lastModified": 1361551979402952,
+ "type": "text/x-moz-place",
+ "uri": "http://example.tld/"
+ },
+ {
+ "guid": "Cfkety492Afk",
+ "title": "test tagged bookmark",
+ "id": 15,
+ "parent": 5,
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "type": "text/x-moz-place",
+ "uri": "http://example.tld/tagged",
+ "tags": "foo"
+ },
+ {
+ "guid": "lOZGoFR1eXbl",
+ "title": "Bookmarks Toolbar Shortcut",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 16,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=TOOLBAR"
+ },
+ {
+ "guid": "7yJWnBVhjRtP",
+ "title": "Folder Shortcut",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 17,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=6"
+ },
+ {
+ "guid": "vm5QXWuWc12l",
+ "title": "Folder Shortcut 2",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 18,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=6123443"
+ },
+ {
+ "guid": "Icg1XlIozA1D",
+ "title": "Folder Shortcut 3",
+ "dateAdded": 1507025843703345,
+ "lastModified": 1507025844703124,
+ "id": 18,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=6&folder=BOOKMARKS_MENU"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/head_bookmarks.js b/toolkit/components/places/tests/unit/head_bookmarks.js
new file mode 100644
index 0000000000..5f6250aa5d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/head_bookmarks.js
@@ -0,0 +1,30 @@
+/* -*- 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.
+const { AddonTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/AddonTestUtils.sys.mjs"
+);
+
+AddonTestUtils.init(this, false);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "42"
+);
+
+add_task(async function setup() {
+ await AddonTestUtils.promiseStartupManager();
+});
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
new file mode 100644
index 0000000000..930b7a8382
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
@@ -0,0 +1,135 @@
+{
+ "guid": "root________",
+ "title": "",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731479000,
+ "id": 1,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731768000,
+ "id": 2,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "X6lUyOspVYwi",
+ "title": "Test Pilot",
+ "index": 0,
+ "dateAdded": 1475084731768000,
+ "lastModified": 1475084731768000,
+ "id": 3,
+ "type": "text/x-moz-place",
+ "uri": "https://testpilot.firefox.com/"
+ },
+ {
+ "guid": "XF4yRP6bTuil",
+ "title": "Mobile bookmarks query",
+ "index": 1,
+ "dateAdded": 1475084731768000,
+ "lastModified": 1475084731768000,
+ "id": 11,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=101"
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "Bookmarks Toolbar",
+ "index": 1,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 4,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder",
+ "children": [
+ {
+ "guid": "buy7711R3ZgE",
+ "title": "MDN",
+ "index": 0,
+ "dateAdded": 1475084731769000,
+ "lastModified": 1475084731769000,
+ "id": 5,
+ "type": "text/x-moz-place",
+ "uri": "https://developer.mozilla.org"
+ }
+ ]
+ },
+ {
+ "guid": "3qmd_imziEBE",
+ "title": "Mobile Bookmarks",
+ "index": 5,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731770000,
+ "id": 101,
+ "annos": [
+ {
+ "name": "mobile/bookmarksRoot",
+ "flags": 0,
+ "expires": 4,
+ "value": 1
+ },
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "A description of the mobile folder that should be ignored on import"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "_o8e1_zxTJFg",
+ "title": "Get Firefox!",
+ "index": 0,
+ "dateAdded": 1475084731769000,
+ "lastModified": 1475084731769000,
+ "id": 7,
+ "type": "text/x-moz-place",
+ "uri": "http://getfirefox.com/"
+ },
+ {
+ "guid": "QCtSqkVYUbXB",
+ "title": "Get Thunderbird!",
+ "index": 1,
+ "dateAdded": 1475084731770000,
+ "lastModified": 1475084731770000,
+ "id": 8,
+ "type": "text/x-moz-place",
+ "uri": "http://getthunderbird.com/"
+ }
+ ]
+ },
+ {
+ "guid": "unfiled_____",
+ "title": "Other Bookmarks",
+ "index": 3,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 9,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder",
+ "children": [
+ {
+ "guid": "KIa9iKZab2Z5",
+ "title": "Add-ons",
+ "index": 0,
+ "dateAdded": 1475084731769000,
+ "lastModified": 1475084731769000,
+ "id": 10,
+ "type": "text/x-moz-place",
+ "uri": "https://addons.mozilla.org"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
new file mode 100644
index 0000000000..8d376bf69c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
@@ -0,0 +1,101 @@
+{
+ "guid": "root________",
+ "title": "",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731479000,
+ "id": 1,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731768000,
+ "id": 2,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "Utodo9b0oVws",
+ "title": "Firefox Accounts",
+ "index": 0,
+ "dateAdded": 1475084731955000,
+ "lastModified": 1475084731955000,
+ "id": 3,
+ "type": "text/x-moz-place",
+ "uri": "https://accounts.firefox.com/"
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "Bookmarks Toolbar",
+ "index": 1,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 4,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder"
+ },
+ {
+ "guid": "3qmd_imziEBE",
+ "title": "Mobile Bookmarks",
+ "index": 5,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731770000,
+ "id": 5,
+ "annos": [
+ {
+ "name": "mobile/bookmarksRoot",
+ "flags": 0,
+ "expires": 4,
+ "value": 1
+ },
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "A description of the mobile folder that should be ignored on import"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "a17yW6-nTxEJ",
+ "title": "Mozilla",
+ "index": 0,
+ "dateAdded": 1475084731959000,
+ "lastModified": 1475084731959000,
+ "id": 6,
+ "type": "text/x-moz-place",
+ "uri": "https://mozilla.org/"
+ },
+ {
+ "guid": "xV10h9Wi3FBM",
+ "title": "Bugzilla",
+ "index": 1,
+ "dateAdded": 1475084731961000,
+ "lastModified": 1475084731961000,
+ "id": 7,
+ "type": "text/x-moz-place",
+ "uri": "https://bugzilla.mozilla.org/"
+ }
+ ]
+ },
+ {
+ "guid": "unfiled_____",
+ "title": "Other Bookmarks",
+ "index": 3,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 8,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder"
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
new file mode 100644
index 0000000000..3c5cb63194
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
@@ -0,0 +1,159 @@
+{
+ "guid": "root________",
+ "title": "",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731479000,
+ "id": 1,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731768000,
+ "id": 2,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "buy7711R3ZgE",
+ "title": "MDN",
+ "index": 0,
+ "dateAdded": 1475084731769000,
+ "lastModified": 1475084731769000,
+ "id": 3,
+ "type": "text/x-moz-place",
+ "uri": "https://developer.mozilla.org"
+ },
+ {
+ "guid": "F_LBgd1fS_uQ",
+ "title": "Mobile bookmarks query for first folder",
+ "index": 1,
+ "dateAdded": 1475084731768000,
+ "lastModified": 1475084731768000,
+ "id": 11,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=101"
+ },
+ {
+ "guid": "oIpmQXMWsXvY",
+ "title": "Mobile bookmarks query for second folder",
+ "index": 2,
+ "dateAdded": 1475084731768000,
+ "lastModified": 1475084731768000,
+ "id": 12,
+ "type": "text/x-moz-place",
+ "uri": "place:folder=102"
+ }
+ ]
+ },
+ {
+ "guid": "3qmd_imziEBE",
+ "title": "Mobile Bookmarks",
+ "index": 5,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731770000,
+ "id": 101,
+ "annos": [
+ {
+ "name": "mobile/bookmarksRoot",
+ "flags": 0,
+ "expires": 4,
+ "value": 1
+ },
+ {
+ "name": "bookmarkProperties/description",
+ "flags": 0,
+ "expires": 4,
+ "mimeType": null,
+ "type": 3,
+ "value": "A description of the mobile folder that should be ignored on import"
+ }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "a17yW6-nTxEJ",
+ "title": "Mozilla",
+ "index": 0,
+ "dateAdded": 1475084731959000,
+ "lastModified": 1475084731959000,
+ "id": 5,
+ "type": "text/x-moz-place",
+ "uri": "https://mozilla.org/"
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "Bookmarks Toolbar",
+ "index": 1,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 6,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder",
+ "children": [
+ {
+ "guid": "Utodo9b0oVws",
+ "title": "Firefox Accounts",
+ "index": 0,
+ "dateAdded": 1475084731955000,
+ "lastModified": 1475084731955000,
+ "id": 7,
+ "type": "text/x-moz-place",
+ "uri": "https://accounts.firefox.com/"
+ }
+ ]
+ },
+ {
+ "guid": "o4YjJpgsufU-",
+ "title": "Mobile Bookmarks",
+ "index": 7,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731770000,
+ "id": 102,
+ "annos": [
+ { "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 }
+ ],
+ "type": "text/x-moz-place-container",
+ "children": [
+ {
+ "guid": "sSZ86WT9WbN3",
+ "title": "DXR",
+ "index": 0,
+ "dateAdded": 1475084731769000,
+ "lastModified": 1475084731769000,
+ "id": 9,
+ "type": "text/x-moz-place",
+ "uri": "https://dxr.mozilla.org"
+ }
+ ]
+ },
+ {
+ "guid": "unfiled_____",
+ "title": "Other Bookmarks",
+ "index": 3,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 10,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder",
+ "children": [
+ {
+ "guid": "xV10h9Wi3FBM",
+ "title": "Bugzilla",
+ "index": 1,
+ "dateAdded": 1475084731961000,
+ "lastModified": 1475084731961000,
+ "id": 11,
+ "type": "text/x-moz-place",
+ "uri": "https://bugzilla.mozilla.org/"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
new file mode 100644
index 0000000000..33908e1fea
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
@@ -0,0 +1,89 @@
+{
+ "guid": "root________",
+ "title": "",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731479000,
+ "id": 1,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731768000,
+ "id": 2,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "X6lUyOspVYwi",
+ "title": "Test Pilot",
+ "index": 0,
+ "dateAdded": 1475084731768000,
+ "lastModified": 1475084731768000,
+ "id": 3,
+ "type": "text/x-moz-place",
+ "uri": "https://testpilot.firefox.com/"
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "Bookmarks Toolbar",
+ "index": 1,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 4,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder"
+ },
+ {
+ "guid": "unfiled_____",
+ "title": "Other Bookmarks",
+ "index": 3,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731742000,
+ "id": 5,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder"
+ },
+ {
+ "guid": "mobile______",
+ "title": "Mobile Bookmarks",
+ "index": 4,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731770000,
+ "id": 6,
+ "annos": [
+ { "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 }
+ ],
+ "type": "text/x-moz-place-container",
+ "root": "mobileFolder",
+ "children": [
+ {
+ "guid": "_o8e1_zxTJFg",
+ "title": "Get Firefox!",
+ "index": 0,
+ "dateAdded": 1475084731769000,
+ "lastModified": 1475084731769000,
+ "id": 7,
+ "type": "text/x-moz-place",
+ "uri": "http://getfirefox.com/"
+ },
+ {
+ "guid": "QCtSqkVYUbXB",
+ "title": "Get Thunderbird!",
+ "index": 1,
+ "dateAdded": 1475084731770000,
+ "lastModified": 1475084731770000,
+ "id": 8,
+ "type": "text/x-moz-place",
+ "uri": "http://getthunderbird.com/"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
new file mode 100644
index 0000000000..97af52c44a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
@@ -0,0 +1,89 @@
+{
+ "guid": "root________",
+ "title": "",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731479000,
+ "id": 1,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "guid": "menu________",
+ "title": "Bookmarks Menu",
+ "index": 0,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731955000,
+ "id": 2,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "guid": "Utodo9b0oVws",
+ "title": "Firefox Accounts",
+ "index": 0,
+ "dateAdded": 1475084731955000,
+ "lastModified": 1475084731955000,
+ "id": 3,
+ "type": "text/x-moz-place",
+ "uri": "https://accounts.firefox.com/"
+ }
+ ]
+ },
+ {
+ "guid": "toolbar_____",
+ "title": "Bookmarks Toolbar",
+ "index": 1,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731938000,
+ "id": 4,
+ "type": "text/x-moz-place-container",
+ "root": "toolbarFolder"
+ },
+ {
+ "guid": "unfiled_____",
+ "title": "Other Bookmarks",
+ "index": 3,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731938000,
+ "id": 5,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder"
+ },
+ {
+ "guid": "mobile______",
+ "title": "Mobile Bookmarks",
+ "index": 4,
+ "dateAdded": 1475084731479000,
+ "lastModified": 1475084731961000,
+ "id": 6,
+ "annos": [
+ { "name": "mobile/bookmarksRoot", "flags": 0, "expires": 4, "value": 1 }
+ ],
+ "type": "text/x-moz-place-container",
+ "root": "mobileFolder",
+ "children": [
+ {
+ "guid": "a17yW6-nTxEJ",
+ "title": "Mozilla",
+ "index": 0,
+ "dateAdded": 1475084731959000,
+ "lastModified": 1475084731959000,
+ "id": 7,
+ "type": "text/x-moz-place",
+ "uri": "https://mozilla.org/"
+ },
+ {
+ "guid": "xV10h9Wi3FBM",
+ "title": "Bugzilla",
+ "index": 1,
+ "dateAdded": 1475084731961000,
+ "lastModified": 1475084731961000,
+ "id": 8,
+ "type": "text/x-moz-place",
+ "uri": "https://bugzilla.mozilla.org/"
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/places/tests/unit/test_1085291.js b/toolkit/components/places/tests/unit/test_1085291.js
new file mode 100644
index 0000000000..b7ec4181f0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1085291.js
@@ -0,0 +1,48 @@
+add_task(async function () {
+ // test that nodes inserted by incremental update for bookmarks of all types
+ // have the extra bookmark properties (bookmarkGuid, dateAdded, lastModified).
+
+ // getFolderContents opens the root node.
+ let root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.toolbarGuid
+ ).root;
+
+ async function insertAndTest(bmInfo) {
+ bmInfo = await PlacesUtils.bookmarks.insert(bmInfo);
+ let node = root.getChild(root.childCount - 1);
+ Assert.equal(node.bookmarkGuid, bmInfo.guid);
+ Assert.equal(node.dateAdded, bmInfo.dateAdded * 1000);
+ Assert.equal(node.lastModified, bmInfo.lastModified * 1000);
+ }
+
+ // Normal bookmark.
+ await insertAndTest({
+ parentGuid: root.bookmarkGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "Test Bookmark",
+ url: "http://test.url.tld",
+ });
+
+ // place: query
+ await insertAndTest({
+ parentGuid: root.bookmarkGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "Test Query",
+ url: `place:parent=${PlacesUtils.bookmarks.menuGuid}`,
+ });
+
+ // folder
+ await insertAndTest({
+ parentGuid: root.bookmarkGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "Test Folder",
+ });
+
+ // separator
+ await insertAndTest({
+ parentGuid: root.bookmarkGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ });
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_1105208.js b/toolkit/components/places/tests/unit/test_1105208.js
new file mode 100644
index 0000000000..6b3f31f96a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1105208.js
@@ -0,0 +1,25 @@
+// Test that result node for folder shortcuts get the target folder title if
+// the shortcut itself has no title set.
+add_task(async function () {
+ let folder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "fake",
+ });
+
+ let shortcutInfo = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: `place:parent=${folder.guid}`,
+ });
+
+ let unfiledRoot = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ ).root;
+ let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.equal(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+
+ Assert.equal(shortcutNode.title, folder.title);
+
+ unfiledRoot.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_1105866.js b/toolkit/components/places/tests/unit/test_1105866.js
new file mode 100644
index 0000000000..c387d9e853
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1105866.js
@@ -0,0 +1,77 @@
+add_task(async function test_folder_shortcuts() {
+ let shortcutInfo = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`,
+ });
+
+ let unfiledRoot = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ ).root;
+ let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(
+ shortcutNode.itemId,
+ await PlacesTestUtils.promiseItemId(shortcutInfo.guid)
+ );
+ Assert.strictEqual(
+ PlacesUtils.asQuery(shortcutNode).folderItemId,
+ await PlacesTestUtils.promiseItemId(PlacesUtils.bookmarks.toolbarGuid)
+ );
+ Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+ Assert.strictEqual(
+ PlacesUtils.asQuery(shortcutNode).targetFolderGuid,
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+
+ // test that a node added incrementally also behaves just as well.
+ shortcutInfo = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: `place:parent=${PlacesUtils.bookmarks.menuGuid}`,
+ });
+ shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(
+ shortcutNode.itemId,
+ await PlacesTestUtils.promiseItemId(shortcutInfo.guid)
+ );
+ Assert.strictEqual(
+ PlacesUtils.asQuery(shortcutNode).folderItemId,
+ await PlacesTestUtils.promiseItemId(PlacesUtils.bookmarks.menuGuid)
+ );
+ Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+ Assert.strictEqual(
+ PlacesUtils.asQuery(shortcutNode).targetFolderGuid,
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ unfiledRoot.containerOpen = false;
+});
+
+add_task(async function test_plain_folder() {
+ let folderInfo = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ let unfiledRoot = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ ).root;
+ let lastChild = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(lastChild.bookmarkGuid, folderInfo.guid);
+ Assert.strictEqual(
+ PlacesUtils.asQuery(lastChild).targetFolderGuid,
+ folderInfo.guid
+ );
+});
+
+add_task(async function test_non_item_query() {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ Assert.strictEqual(root.itemId, -1);
+ Assert.strictEqual(PlacesUtils.asQuery(root).folderItemId, -1);
+ Assert.strictEqual(root.bookmarkGuid, "");
+ Assert.strictEqual(PlacesUtils.asQuery(root).targetFolderGuid, "");
+});
diff --git a/toolkit/components/places/tests/unit/test_1606731.js b/toolkit/components/places/tests/unit/test_1606731.js
new file mode 100644
index 0000000000..89e8ec0498
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1606731.js
@@ -0,0 +1,21 @@
+/* -*- 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/. */
+
+let hs = PlacesUtils.history;
+
+/* Test for diacritic-insensitive history search */
+
+add_task(async function test_execute() {
+ const TEST_URL = "http://example.net/El_%C3%81rea_51";
+ const SEARCH_TERM = "area";
+ await PlacesTestUtils.addVisits(uri(TEST_URL));
+ let query = hs.getNewQuery();
+ query.searchTerms = SEARCH_TERM;
+ let options = hs.getNewQueryOptions();
+ let result = hs.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.ok(result.root.childCount == 1);
+});
diff --git a/toolkit/components/places/tests/unit/test_331487.js b/toolkit/components/places/tests/unit/test_331487.js
new file mode 100644
index 0000000000..2d4f5f8279
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_331487.js
@@ -0,0 +1,113 @@
+/* -*- 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 history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+add_task(async function test_hierarchical_query() {
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "1 title",
+ url: "http://a1.com/",
+ },
+ {
+ title: "subfolder 1",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "2 title",
+ url: "http://a2.com/",
+ },
+ {
+ title: "subfolder 2",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "3 title",
+ url: "http://a3.com/",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ let [folderGuid, b1, sf1, b2, sf2, b3] = bookmarks.map(
+ bookmark => bookmark.guid
+ );
+
+ // bookmark query that should result in the "hierarchical" result
+ // because there is one query, one folder,
+ // no begin time, no end time, no domain, no uri, no search term
+ // and no max results. See GetSimpleBookmarksQueryFolder()
+ // for more details.
+ var options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ var query = histsvc.getNewQuery();
+ query.setParents([folderGuid]);
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 2);
+ Assert.equal(root.getChild(0).bookmarkGuid, b1);
+ Assert.equal(root.getChild(1).bookmarkGuid, sf1);
+
+ // check the contents of the subfolder
+ var sf1Node = root.getChild(1);
+ sf1Node = sf1Node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ sf1Node.containerOpen = true;
+ Assert.equal(sf1Node.childCount, 2);
+ Assert.equal(sf1Node.getChild(0).bookmarkGuid, b2);
+ Assert.equal(sf1Node.getChild(1).bookmarkGuid, sf2);
+
+ // check the contents of the subfolder's subfolder
+ var sf2Node = sf1Node.getChild(1);
+ sf2Node = sf2Node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ sf2Node.containerOpen = true;
+ Assert.equal(sf2Node.childCount, 1);
+ Assert.equal(sf2Node.getChild(0).bookmarkGuid, b3);
+
+ sf2Node.containerOpen = false;
+ sf1Node.containerOpen = false;
+ root.containerOpen = false;
+
+ // bookmark query that should result in a flat list
+ // because we specified max results
+ options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.maxResults = 10;
+ query = histsvc.getNewQuery();
+ query.setParents([folderGuid]);
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 3);
+ Assert.equal(root.getChild(0).bookmarkGuid, b1);
+ Assert.equal(root.getChild(1).bookmarkGuid, b2);
+ Assert.equal(root.getChild(2).bookmarkGuid, b3);
+ root.containerOpen = false;
+
+ // XXX TODO
+ // test that if we have: more than one query,
+ // multiple folders, a begin time, an end time, a domain, a uri
+ // or a search term, that we get the (correct) flat list results
+ // (like we do when specified maxResults)
+});
diff --git a/toolkit/components/places/tests/unit/test_384370.js b/toolkit/components/places/tests/unit/test_384370.js
new file mode 100644
index 0000000000..cc760e3276
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -0,0 +1,188 @@
+var tagData = [
+ { uri: uri("http://slint.us"), tags: ["indie", "kentucky", "music"] },
+ {
+ uri: uri("http://en.wikipedia.org/wiki/Diplodocus"),
+ tags: ["dinosaur", "dj", "rad word"],
+ },
+];
+
+var bookmarkData = [
+ { uri: uri("http://slint.us"), title: "indie,kentucky,music" },
+ {
+ uri: uri("http://en.wikipedia.org/wiki/Diplodocus"),
+ title: "dinosaur,dj,rad word",
+ },
+];
+
+/*
+ HTML+FEATURES SUMMARY:
+ - import legacy bookmarks
+ - export as json, import, test (tests integrity of html > json)
+ - export as html, import, test (tests integrity of json > html)
+
+ BACKUP/RESTORE SUMMARY:
+ - create a bookmark in each root
+ - tag multiple URIs with multiple tags
+ - export as json, import, test
+*/
+add_task(async function () {
+ // Remove eventual bookmarks.exported.json.
+ let jsonFile = PathUtils.join(
+ PathUtils.profileDir,
+ "bookmarks.exported.json"
+ );
+ await IOUtils.remove(jsonFile, { ignoreAbsent: true });
+
+ // Test importing a pre-Places canonical bookmarks file.
+ // Note: we do not empty the db before this import to catch bugs like 380999
+ let htmlFile = PathUtils.join(do_get_cwd().path, "bookmarks.preplaces.html");
+ await BookmarkHTMLUtils.importFromFile(htmlFile, { replace: true });
+
+ // Populate the database.
+ for (let { uri, tags } of tagData) {
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
+ for (let { uri, title } of bookmarkData) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title,
+ });
+ }
+ for (let { uri, title } of bookmarkData) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: uri,
+ title,
+ });
+ }
+
+ await validate("initial database");
+
+ // Test exporting a Places canonical json file.
+ // 1. export to bookmarks.exported.json
+ await BookmarkJSONUtils.exportToFile(jsonFile);
+ info("exported json");
+
+ // 2. empty bookmarks db
+ // 3. import bookmarks.exported.json
+ await BookmarkJSONUtils.importFromFile(jsonFile, { replace: true });
+ info("imported json");
+
+ // 4. run the test-suite
+ await validate("re-imported json");
+ info("validated import");
+});
+
+async function validate(infoMsg) {
+ info(`Validating ${infoMsg}: testMenuBookmarks`);
+ await testMenuBookmarks();
+ info(`Validating ${infoMsg}: testToolbarBookmarks`);
+ await testToolbarBookmarks();
+ info(`Validating ${infoMsg}: testUnfiledBookmarks`);
+ testUnfiledBookmarks();
+ info(`Validating ${infoMsg}: testTags`);
+ testTags();
+ await PlacesTestUtils.promiseAsyncUpdates();
+}
+
+// Tests a bookmarks datastore that has a set of bookmarks, etc
+// that flex each supported field and feature.
+async function testMenuBookmarks() {
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root;
+ Assert.equal(root.childCount, 3);
+
+ let separatorNode = root.getChild(1);
+ Assert.equal(separatorNode.type, separatorNode.RESULT_TYPE_SEPARATOR);
+
+ let folderNode = root.getChild(2);
+ Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
+ Assert.equal(folderNode.title, "test");
+ let folder = await PlacesUtils.bookmarks.fetch(folderNode.bookmarkGuid);
+ Assert.equal(folder.dateAdded.getTime(), 1177541020000);
+
+ Assert.equal(PlacesUtils.asQuery(folderNode).hasChildren, true);
+
+ // open test folder, and test the children
+ folderNode.containerOpen = true;
+ Assert.equal(folderNode.childCount, 1);
+
+ let bookmarkNode = folderNode.getChild(0);
+ Assert.equal("http://test/post", bookmarkNode.uri);
+ Assert.equal("test post keyword", bookmarkNode.title);
+ Assert.equal(bookmarkNode.dateAdded, 1177375336000000);
+
+ let entry = await PlacesUtils.keywords.fetch({ url: bookmarkNode.uri });
+ Assert.equal("test", entry.keyword);
+ Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData);
+
+ let pageInfo = await PlacesUtils.history.fetch(bookmarkNode.uri, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ "ISO-8859-1",
+ "Should have the correct charset"
+ );
+
+ folderNode.containerOpen = false;
+ root.containerOpen = false;
+}
+
+async function testToolbarBookmarks() {
+ let root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.toolbarGuid
+ ).root;
+
+ // child count (add 2 for pre-existing items, one of the feeds is skipped
+ // because it doesn't have href)
+ Assert.equal(root.childCount, bookmarkData.length + 2);
+
+ // Livemarks are no more supported but may still exist in old html files.
+ let legacyLivemarkNode = root.getChild(1);
+ Assert.equal("Latest Headlines", legacyLivemarkNode.title);
+ Assert.equal(
+ "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ legacyLivemarkNode.uri
+ );
+ Assert.equal(
+ legacyLivemarkNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_URI
+ );
+
+ // test added bookmark data
+ let bookmarkNode = root.getChild(2);
+ Assert.equal(bookmarkNode.uri, bookmarkData[0].uri.spec);
+ Assert.equal(bookmarkNode.title, bookmarkData[0].title);
+ bookmarkNode = root.getChild(3);
+ Assert.equal(bookmarkNode.uri, bookmarkData[1].uri.spec);
+ Assert.equal(bookmarkNode.title, bookmarkData[1].title);
+
+ root.containerOpen = false;
+}
+
+function testUnfiledBookmarks() {
+ let root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ ).root;
+ // child count (add 1 for pre-existing item)
+ Assert.equal(root.childCount, bookmarkData.length + 1);
+ for (let i = 1; i < root.childCount; ++i) {
+ let child = root.getChild(i);
+ Assert.equal(child.uri, bookmarkData[i - 1].uri.spec);
+ Assert.equal(child.title, bookmarkData[i - 1].title);
+ if (child.tags) {
+ Assert.equal(child.tags, bookmarkData[i - 1].title);
+ }
+ }
+ root.containerOpen = false;
+}
+
+function testTags() {
+ for (let { uri, tags } of tagData) {
+ info("Test tags for " + uri.spec + ": " + tags + "\n");
+ let foundTags = PlacesUtils.tagging.getTagsForURI(uri);
+ Assert.equal(foundTags.length, tags.length);
+ Assert.ok(tags.every(tag => foundTags.includes(tag)));
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_385397.js b/toolkit/components/places/tests/unit/test_385397.js
new file mode 100644
index 0000000000..7746b89657
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_385397.js
@@ -0,0 +1,152 @@
+/* -*- 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/. */
+
+const TOTAL_SITES = 20;
+
+add_task(async function test_execute() {
+ let now = (Date.now() - 10000) * 1000;
+
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test-" + i + ".com/";
+ let testURI = uri(site);
+ let testImageURI = uri(site + "blank.gif");
+ let when = now + i * TOTAL_SITES * 1000;
+ await PlacesTestUtils.addVisits([
+ { uri: testURI, visitDate: when, transition: TRANSITION_TYPED },
+ {
+ uri: testImageURI,
+ visitDate: when + 1000,
+ transition: TRANSITION_EMBED,
+ },
+ {
+ uri: testImageURI,
+ visitDate: when + 2000,
+ transition: TRANSITION_FRAMED_LINK,
+ },
+ { uri: testURI, visitDate: when + 3000, transition: TRANSITION_LINK },
+ ]);
+ }
+
+ // verify our visits AS_VISIT, ordered by date descending
+ // including hidden
+ // we should get 80 visits:
+ // http://www.test-19.com/
+ // http://www.test-19.com/blank.gif
+ // http://www.test-19.com/
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-0.com/
+ // http://www.test-0.com/blank.gif
+ // http://www.test-0.com/
+ // http://www.test-0.com/
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ options.includeHidden = true;
+ let root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ // Embed visits are not added to the database, thus they won't appear.
+ Assert.equal(cc, 3 * TOTAL_SITES);
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let index = i * 3;
+ let node = root.getChild(index);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ Assert.equal(node.uri, site + "blank.gif");
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // verify our visits AS_VISIT, ordered by date descending
+ // we should get 40 visits:
+ // http://www.test-19.com/
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-0.com/
+ // http://www.test-0.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ // 2 * TOTAL_SITES because we count the TYPED and LINK, but not EMBED or FRAMED
+ Assert.equal(cc, 2 * TOTAL_SITES);
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let index = i * 2;
+ let node = root.getChild(index);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // test our optimized query for the places menu
+ // place:type=0&sort=4&maxResults=10
+ // verify our visits AS_URI, ordered by date descending
+ // we should get 10 visits:
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.maxResults = 10;
+ options.resultType = options.RESULTS_AS_URI;
+ root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ Assert.equal(cc, options.maxResults);
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // test without a maxResults, which executes a different query
+ // but the first 10 results should be the same.
+ // verify our visits AS_URI, ordered by date descending
+ // we should get 20 visits, but the first 10 should be
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ Assert.equal(cc, TOTAL_SITES);
+ for (let i = 0; i < 10; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_399266.js b/toolkit/components/places/tests/unit/test_399266.js
new file mode 100644
index 0000000000..6f99c710f8
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_399266.js
@@ -0,0 +1,82 @@
+/* -*- 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/. */
+
+const TOTAL_SITES = 20;
+
+add_task(async function test_execute() {
+ let places = [];
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ for (let j = 0; j <= i; j++) {
+ places.push({
+ uri: uri("http://www.test-" + i + ".com/"),
+ transition: TRANSITION_TYPED,
+ });
+ // because these are embedded visits, they should not show up on our
+ // query results. If they do, we have a problem.
+ places.push({
+ uri: uri("http://www.hidden.com/hidden.gif"),
+ transition: TRANSITION_EMBED,
+ });
+ places.push({
+ uri: uri("http://www.alsohidden.com/hidden.gif"),
+ transition: TRANSITION_FRAMED_LINK,
+ });
+ }
+ }
+ await PlacesTestUtils.addVisits(places);
+
+ // test our optimized query for the "Most Visited" item
+ // in the "Smart Bookmarks" folder
+ // place:queryType=0&sort=8&maxResults=10
+ // verify our visits AS_URI, ordered by visit count descending
+ // we should get 10 visits:
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+ options.maxResults = 10;
+ options.resultType = options.RESULTS_AS_URI;
+ let root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ Assert.equal(cc, options.maxResults);
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // test without a maxResults, which executes a different query
+ // but the first 10 results should be the same.
+ // verify our visits AS_URI, ordered by visit count descending
+ // we should get 20 visits, but the first 10 should be
+ // http://www.test-19.com/
+ // ...
+ // http://www.test-10.com/
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+ options.resultType = options.RESULTS_AS_URI;
+ root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ root.containerOpen = true;
+ cc = root.childCount;
+ Assert.equal(cc, TOTAL_SITES);
+ for (let i = 0; i < 10; i++) {
+ let node = root.getChild(i);
+ let site = "http://www.test-" + (TOTAL_SITES - 1 - i) + ".com/";
+ Assert.equal(node.uri, site);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_402799.js b/toolkit/components/places/tests/unit/test_402799.js
new file mode 100644
index 0000000000..f621911cce
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_402799.js
@@ -0,0 +1,60 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Get history services
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+} catch (ex) {
+ do_throw("Could not get history services\n");
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService(
+ Ci.nsITaggingService
+ );
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+add_task(async function test_query_only_returns_bookmarks_not_tags() {
+ const url = "http://foo.bar/";
+
+ // create 2 bookmarks on the same uri
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "title 1",
+ url,
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "title 2",
+ url,
+ });
+ // add some tags
+ tagssvc.tagURI(uri(url), ["foo", "bar", "foobar", "foo bar"]);
+
+ // check that a generic bookmark query returns only real bookmarks
+ let options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ let query = histsvc.getNewQuery();
+ let result = histsvc.executeQuery(query, options);
+ let root = result.root;
+
+ root.containerOpen = true;
+ let cc = root.childCount;
+ Assert.equal(cc, 2);
+ let node1 = root.getChild(0);
+ node1 = await PlacesUtils.bookmarks.fetch(node1.bookmarkGuid);
+ Assert.equal(node1.parentGuid, PlacesUtils.bookmarks.menuGuid);
+ let node2 = root.getChild(1);
+ node2 = await PlacesUtils.bookmarks.fetch(node2.bookmarkGuid);
+ Assert.equal(node2.parentGuid, PlacesUtils.bookmarks.toolbarGuid);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_412132.js b/toolkit/components/places/tests/unit/test_412132.js
new file mode 100644
index 0000000000..65653b5c0a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_412132.js
@@ -0,0 +1,181 @@
+/* -*- 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 DESCRIPTION:
+ *
+ * Tests patch to Bug 412132:
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=412132
+ */
+
+const TEST_URL0 = "http://example.com/";
+const TEST_URL1 = "http://example.com/1";
+const TEST_URL2 = "http://example.com/2";
+
+add_task(async function changeuri_unvisited_bookmark() {
+ info(
+ "After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should be zero if original URI is unvisited and " +
+ "no longer bookmarked."
+ );
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URL1,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL1,
+ }),
+ 0,
+ "Bookmarked => frecency of URI should be != 0"
+ );
+
+ await PlacesUtils.bookmarks.update({
+ guid: bookmark.guid,
+ url: TEST_URL2,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL1,
+ }),
+ 0,
+ "Unvisited URI no longer bookmarked => frecency should = 0"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function changeuri_visited_bookmark() {
+ info(
+ "After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should not be zero if original URI is visited."
+ );
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URL1,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL1,
+ }),
+ 0,
+ "Bookmarked => frecency of URI should be != 0"
+ );
+
+ await PlacesTestUtils.addVisits(TEST_URL1);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ await PlacesUtils.bookmarks.update({
+ guid: bookmark.guid,
+ url: TEST_URL2,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL1,
+ }),
+ 0,
+ "*Visited* URI no longer bookmarked => frecency should != 0"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function changeuri_bookmark_still_bookmarked() {
+ info(
+ "After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should not be zero if original URI is still " +
+ "bookmarked."
+ );
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 1 title",
+ url: TEST_URL1,
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 2 title",
+ url: TEST_URL1,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL1,
+ }),
+ 0,
+ "Bookmarked => frecency of URI should be != 0"
+ );
+
+ await PlacesUtils.bookmarks.update({
+ guid: bookmark.guid,
+ url: TEST_URL2,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ info("URI still bookmarked => frecency should != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL2,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function changeuri_nonexistent_bookmark() {
+ // Try a bogus guid.
+ await Assert.rejects(
+ PlacesUtils.bookmarks.update({
+ guid: "ABCDEDFGHIJK",
+ url: TEST_URL2,
+ }),
+ /No bookmarks found for the provided GUID/,
+ "Changing the URI of a non-existent bookmark should fail."
+ );
+
+ // Now add a bookmark, delete it, and check.
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URL0,
+ });
+
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+
+ await Assert.rejects(
+ PlacesUtils.bookmarks.update({
+ guid: bookmark.guid,
+ url: TEST_URL2,
+ }),
+ /No bookmarks found for the provided GUID/,
+ "Changing the URI of a non-existent bookmark should fail."
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
diff --git a/toolkit/components/places/tests/unit/test_415460.js b/toolkit/components/places/tests/unit/test_415460.js
new file mode 100644
index 0000000000..3f0f7a1edb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_415460.js
@@ -0,0 +1,37 @@
+/* -*- 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 hs = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+);
+
+/**
+ * Checks to see that a search has exactly one result in the database.
+ *
+ * @param aTerms
+ * The terms to search for.
+ * @returns true if the search returns one result, false otherwise.
+ */
+function search_has_result(aTerms) {
+ var options = hs.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI;
+ var query = hs.getNewQuery();
+ query.searchTerms = aTerms;
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ root.containerOpen = false;
+ return cc == 1;
+}
+
+add_task(async function test_execute() {
+ const SEARCH_TERM = "ユニコード";
+ const TEST_URL = "http://example.com/" + SEARCH_TERM + "/";
+ await PlacesTestUtils.addVisits(uri(TEST_URL));
+ Assert.ok(search_has_result(SEARCH_TERM));
+});
diff --git a/toolkit/components/places/tests/unit/test_415757.js b/toolkit/components/places/tests/unit/test_415757.js
new file mode 100644
index 0000000000..a069bc6aa3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_415757.js
@@ -0,0 +1,92 @@
+/* -*- 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/. */
+
+/**
+ * Checks to see that a URI is in the database.
+ *
+ * @param aURI
+ * The URI to check.
+ * @returns true if the URI is in the DB, false otherwise.
+ */
+function uri_in_db(aURI) {
+ var options = PlacesUtils.history.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI;
+ var query = PlacesUtils.history.getNewQuery();
+ query.uri = aURI;
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ root.containerOpen = false;
+ return cc == 1;
+}
+
+const TOTAL_SITES = 20;
+
+// main
+add_task(async function test_execute() {
+ // add pages to global history
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let uri = "http://www.test-" + i + ".com/";
+ let when = Date.now() * 1000 + i * TOTAL_SITES;
+ await PlacesTestUtils.addVisits({ uri, visitDate: when });
+ }
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let uri = "http://www.test.com/" + i + "/";
+ let when = Date.now() * 1000 + i * TOTAL_SITES;
+ await PlacesTestUtils.addVisits({ uri, visitDate: when });
+ }
+
+ // set a page annotation on one of the urls that will be removed
+ var testAnnoDeletedURI = "http://www.test.com/1/";
+ var testAnnoDeletedName = "foo";
+ var testAnnoDeletedValue = "bar";
+ await PlacesUtils.history.update({
+ url: testAnnoDeletedURI,
+ annotations: new Map([[testAnnoDeletedName, testAnnoDeletedValue]]),
+ });
+
+ // set a page annotation on one of the urls that will NOT be removed
+ var testAnnoRetainedURI = "http://www.test-1.com/";
+ var testAnnoRetainedName = "foo";
+ var testAnnoRetainedValue = "bar";
+ await PlacesUtils.history.update({
+ url: testAnnoRetainedURI,
+ annotations: new Map([[testAnnoRetainedName, testAnnoRetainedValue]]),
+ });
+
+ // remove pages from www.test.com
+ await PlacesUtils.history.removeByFilter({ host: "www.test.com" });
+
+ // check that all pages in www.test.com have been removed
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test.com/" + i + "/";
+ let testURI = uri(site);
+ Assert.ok(!uri_in_db(testURI));
+ }
+
+ // check that all pages in www.test-X.com have NOT been removed
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test-" + i + ".com/";
+ let testURI = uri(site);
+ Assert.ok(uri_in_db(testURI));
+ }
+
+ // check that annotation on the removed item does not exists
+ await assertNoOrphanPageAnnotations();
+
+ // check that annotation on the NOT removed item still exists
+ let pageInfo = await PlacesUtils.history.fetch(testAnnoRetainedURI, {
+ includeAnnotations: true,
+ });
+
+ Assert.equal(
+ pageInfo.annotations.get(testAnnoRetainedName),
+ testAnnoRetainedValue,
+ "Should have kept the annotation for the non-removed items"
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_419792_node_tags_property.js b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js
new file mode 100644
index 0000000000..53b0b74fce
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js
@@ -0,0 +1,52 @@
+/* -*- 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 services
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+);
+var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService(
+ Ci.nsITaggingService
+);
+
+add_task(async function test_query_node_tags_property() {
+ // get toolbar node
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ var result = histsvc.executeQuery(query, options);
+ var toolbarNode = result.root;
+ toolbarNode.containerOpen = true;
+
+ // add a bookmark
+ var bookmarkURI = uri("http://foo.com");
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "",
+ url: bookmarkURI,
+ });
+
+ // get the node for the new bookmark
+ var node = toolbarNode.getChild(toolbarNode.childCount - 1);
+ Assert.equal(node.bookmarkGuid, bookmark.guid);
+
+ // confirm there's no tags via the .tags property
+ Assert.equal(node.tags, null);
+
+ // add a tag
+ tagssvc.tagURI(bookmarkURI, ["foo"]);
+ Assert.equal(node.tags, "foo");
+
+ // add another tag, to test delimiter and sorting
+ tagssvc.tagURI(bookmarkURI, ["bar"]);
+ Assert.equal(node.tags, "bar,foo");
+
+ // remove the tags, confirming the property is cleared
+ tagssvc.untagURI(bookmarkURI, null);
+ Assert.equal(node.tags, null);
+
+ toolbarNode.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_425563.js b/toolkit/components/places/tests/unit/test_425563.js
new file mode 100644
index 0000000000..8ecf8bbcc8
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_425563.js
@@ -0,0 +1,76 @@
+/* -*- 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 test_execute() {
+ let count_visited_URIs = [
+ "http://www.test-link.com/",
+ "http://www.test-typed.com/",
+ "http://www.test-bookmark.com/",
+ "http://www.test-redirect-permanent.com/",
+ "http://www.test-redirect-temporary.com/",
+ ];
+
+ let notcount_visited_URIs = [
+ "http://www.test-download.com/",
+ "http://www.test-framed.com/",
+ "http://www.test-reload.com/",
+ ];
+
+ // add visits, one for each transition type
+ await PlacesTestUtils.addVisits([
+ { uri: uri("http://www.test-link.com/"), transition: TRANSITION_LINK },
+ { uri: uri("http://www.test-typed.com/"), transition: TRANSITION_TYPED },
+ {
+ uri: uri("http://www.test-bookmark.com/"),
+ transition: TRANSITION_BOOKMARK,
+ },
+ {
+ uri: uri("http://www.test-framed.com/"),
+ transition: TRANSITION_FRAMED_LINK,
+ },
+ {
+ uri: uri("http://www.test-redirect-permanent.com/"),
+ transition: TRANSITION_REDIRECT_PERMANENT,
+ },
+ {
+ uri: uri("http://www.test-redirect-temporary.com/"),
+ transition: TRANSITION_REDIRECT_TEMPORARY,
+ },
+ {
+ uri: uri("http://www.test-download.com/"),
+ transition: TRANSITION_DOWNLOAD,
+ },
+ { uri: uri("http://www.test-reload.com/"), transition: TRANSITION_RELOAD },
+ ]);
+
+ // check that all links are marked as visited
+ for (let visited_uri of count_visited_URIs) {
+ Assert.ok(await PlacesUtils.history.hasVisits(uri(visited_uri)));
+ }
+ for (let visited_uri of notcount_visited_URIs) {
+ Assert.ok(await PlacesUtils.history.hasVisits(uri(visited_uri)));
+ }
+
+ // check that visit_count does not take in count embed and downloads
+ // maxVisits query are directly binded to visit_count
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_VISITCOUNT_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ options.includeHidden = true;
+ let query = PlacesUtils.history.getNewQuery();
+ query.minVisits = 1;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+
+ root.containerOpen = true;
+ let cc = root.childCount;
+ Assert.equal(cc, count_visited_URIs.length);
+
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ Assert.notEqual(count_visited_URIs.indexOf(node.uri), -1);
+ }
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js
new file mode 100644
index 0000000000..d5926d0c17
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.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/. */
+
+/*
+
+- add a folder
+- add a folder-shortcut to the new folder
+- query for the shortcut
+- remove the folder-shortcut
+- confirm the shortcut is removed from the query results
+
+*/
+
+add_task(async function test_query_with_remove_shortcut() {
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+
+ let query = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "",
+ url: `place:parent=${folder.guid}`,
+ });
+
+ var root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.toolbarGuid,
+ false,
+ true
+ ).root;
+
+ var oldCount = root.childCount;
+
+ await PlacesUtils.bookmarks.remove(query.guid);
+
+ Assert.equal(root.childCount, oldCount - 1);
+
+ root.containerOpen = false;
+
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
diff --git a/toolkit/components/places/tests/unit/test_433317_query_title_update.js b/toolkit/components/places/tests/unit/test_433317_query_title_update.js
new file mode 100644
index 0000000000..d8f69064d9
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_433317_query_title_update.js
@@ -0,0 +1,43 @@
+/* -*- 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 test_query_title_update() {
+ try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+ } catch (ex) {
+ do_throw("Unable to initialize Places services");
+ }
+
+ // create a query bookmark
+ let bmQuery = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "test query",
+ url: "place:",
+ });
+
+ // query for that query
+ var options = histsvc.getNewQueryOptions();
+ let query = histsvc.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var queryNode = root.getChild(0);
+ Assert.equal(queryNode.title, "test query");
+
+ // change the title
+ await PlacesUtils.bookmarks.update({
+ guid: bmQuery.guid,
+ title: "foo",
+ });
+
+ // confirm the node was updated
+ Assert.equal(queryNode.title, "foo");
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js
new file mode 100644
index 0000000000..231722e72b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js
@@ -0,0 +1,52 @@
+/* -*- 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 test_execute() {
+ try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+ } catch (ex) {
+ do_throw("Unable to initialize Places services");
+ }
+
+ // add a visit
+ var testURI = uri("http://test");
+ await PlacesTestUtils.addVisits(testURI);
+
+ // query for the visit
+ var options = histsvc.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI;
+ var query = histsvc.getNewQuery();
+ query.uri = testURI;
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+
+ // check hasChildren while the container is closed
+ Assert.equal(root.hasChildren, true);
+
+ // now check via the saved search path
+ var queryURI = histsvc.queryToQueryString(query, options);
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "test query",
+ url: queryURI,
+ });
+
+ // query for that query
+ options = histsvc.getNewQueryOptions();
+ query = histsvc.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ var queryNode = root.getChild(0);
+ Assert.equal(queryNode.title, "test query");
+ queryNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ Assert.equal(queryNode.hasChildren, true);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_454977.js b/toolkit/components/places/tests/unit/test_454977.js
new file mode 100644
index 0000000000..aa6437988f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_454977.js
@@ -0,0 +1,121 @@
+/* -*- 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/. */
+
+// Cache actual visit_count value, filled by add_visit, used by check_results
+var visit_count = 0;
+
+// Returns the Place ID corresponding to an added visit.
+async function task_add_visit(aURI, aVisitType) {
+ // Wait for a visits notification and get the visitId.
+ let visitId;
+ let visitsPromise = PlacesTestUtils.waitForNotification(
+ "page-visited",
+ visits => {
+ visitId = visits[0].visitId;
+ let { url } = visits[0];
+ return url == aURI.spec;
+ }
+ );
+
+ // Add visits.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: aURI,
+ transition: aVisitType,
+ },
+ ]);
+
+ if (aVisitType != TRANSITION_EMBED) {
+ await visitsPromise;
+ }
+
+ // Increase visit_count if applicable
+ if (
+ aVisitType != 0 &&
+ aVisitType != TRANSITION_EMBED &&
+ aVisitType != TRANSITION_FRAMED_LINK &&
+ aVisitType != TRANSITION_DOWNLOAD &&
+ aVisitType != TRANSITION_RELOAD
+ ) {
+ visit_count++;
+ }
+
+ // Get the place id
+ if (visitId > 0) {
+ let sql = "SELECT place_id FROM moz_historyvisits WHERE id = ?1";
+ let stmt = DBConn().createStatement(sql);
+ stmt.bindByIndex(0, visitId);
+ Assert.ok(stmt.executeStep());
+ let placeId = stmt.getInt64(0);
+ stmt.finalize();
+ Assert.ok(placeId > 0);
+ return placeId;
+ }
+ return 0;
+}
+
+/**
+ * Checks for results consistency, using visit_count as constraint
+ * @param aExpectedCount
+ * Number of history results we are expecting (excluded hidden ones)
+ * @param aExpectedCountWithHidden
+ * Number of history results we are expecting (included hidden ones)
+ */
+function check_results(aExpectedCount, aExpectedCountWithHidden) {
+ let query = PlacesUtils.history.getNewQuery();
+ // used to check visit_count
+ query.minVisits = visit_count;
+ query.maxVisits = visit_count;
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ // Children without hidden ones
+ Assert.equal(root.childCount, aExpectedCount);
+ root.containerOpen = false;
+
+ // Execute again with includeHidden = true
+ // This will ensure visit_count is correct
+ options.includeHidden = true;
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ // Children with hidden ones
+ Assert.equal(root.childCount, aExpectedCountWithHidden);
+ root.containerOpen = false;
+}
+
+// main
+
+add_task(async function test_execute() {
+ const TEST_URI = uri("http://test.mozilla.org/");
+
+ // Add a visit that force hidden
+ await task_add_visit(TEST_URI, TRANSITION_EMBED);
+ check_results(0, 0);
+
+ let placeId = await task_add_visit(TEST_URI, TRANSITION_FRAMED_LINK);
+ check_results(0, 1);
+
+ // Add a visit that force unhide and check the place id.
+ // - We expect that the place gets hidden = 0 while retaining the same
+ // place id and a correct visit_count.
+ Assert.equal(await task_add_visit(TEST_URI, TRANSITION_TYPED), placeId);
+ check_results(1, 1);
+
+ // Add a visit that should not increase visit_count
+ Assert.equal(await task_add_visit(TEST_URI, TRANSITION_RELOAD), placeId);
+ check_results(1, 1);
+
+ // Add a visit that should not increase visit_count
+ Assert.equal(await task_add_visit(TEST_URI, TRANSITION_DOWNLOAD), placeId);
+ check_results(1, 1);
+
+ // Add a visit, check that hidden is not overwritten
+ // - We expect that the place has still hidden = 0, while retaining
+ // correct visit_count.
+ await task_add_visit(TEST_URI, TRANSITION_EMBED);
+ check_results(1, 1);
+});
diff --git a/toolkit/components/places/tests/unit/test_463863.js b/toolkit/components/places/tests/unit/test_463863.js
new file mode 100644
index 0000000000..d524c00cd3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_463863.js
@@ -0,0 +1,56 @@
+/* -*- 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 DESCRIPTION:
+ *
+ * This test checks that in a basic history query all transition types visits
+ * appear but TRANSITION_EMBED and TRANSITION_FRAMED_LINK ones.
+ */
+
+var transitions = [
+ TRANSITION_LINK,
+ TRANSITION_TYPED,
+ TRANSITION_BOOKMARK,
+ TRANSITION_EMBED,
+ TRANSITION_FRAMED_LINK,
+ TRANSITION_REDIRECT_PERMANENT,
+ TRANSITION_REDIRECT_TEMPORARY,
+ TRANSITION_DOWNLOAD,
+];
+
+function runQuery(aResultType) {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = aResultType;
+ let root = PlacesUtils.history.executeQuery(
+ PlacesUtils.history.getNewQuery(),
+ options
+ ).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ Assert.equal(cc, transitions.length - 2);
+
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ // Check that all transition types but EMBED and FRAMED appear in results
+ Assert.notEqual(node.uri.substr(6, 1), TRANSITION_EMBED);
+ Assert.notEqual(node.uri.substr(6, 1), TRANSITION_FRAMED_LINK);
+ }
+ root.containerOpen = false;
+}
+
+add_task(async function test_execute() {
+ // add visits, one for each transition type
+ for (let transition of transitions) {
+ await PlacesTestUtils.addVisits({
+ uri: uri("http://" + transition + ".mozilla.org/"),
+ transition,
+ });
+ }
+
+ runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_VISIT);
+ runQuery(Ci.nsINavHistoryQueryOptions.RESULTS_AS_URI);
+});
diff --git a/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js
new file mode 100644
index 0000000000..73bb5e9af4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js
@@ -0,0 +1,19 @@
+/* -*- 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/. */
+
+function run_test() {
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = options.RESULT_TYPE_QUERY;
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ Assert.equal(
+ PlacesUtils.asQuery(root).query.uri,
+ null,
+ "Should be null and not crash the browser"
+ );
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
new file mode 100644
index 0000000000..221377e184
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
@@ -0,0 +1,130 @@
+/* -*- 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 DESCRIPTION:
+ *
+ * This test checks that setting a sort on a RESULTS_AS_DATE_QUERY query,
+ * children of inside containers are sorted accordingly.
+ */
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+);
+
+// Will be inserted in this order, so last one will be the newest visit.
+var pages = [
+ "http://a.mozilla.org/1/",
+ "http://a.mozilla.org/2/",
+ "http://a.mozilla.org/3/",
+ "http://a.mozilla.org/4/",
+ "http://b.mozilla.org/5/",
+ "http://b.mozilla.org/6/",
+ "http://b.mozilla.org/7/",
+ "http://b.mozilla.org/8/",
+];
+
+add_task(async function test_initialize() {
+ // Add visits.
+ let now = new Date();
+ for (let pageIndex = 0; pageIndex < pages.length; ++pageIndex) {
+ let page = pages[pageIndex];
+ await PlacesTestUtils.addVisits({
+ uri: uri(page),
+ visitDate: new Date(now - (pages.length - pageIndex)),
+ });
+ }
+});
+
+/**
+ * Tests that sorting date query by none will sort by title asc.
+ */
+add_task(function () {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_QUERY;
+ // This should sort by title asc.
+ options.sortingMode = options.SORT_BY_NONE;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var dayContainer = root
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayContainer.containerOpen = true;
+
+ var cc = dayContainer.childCount;
+ Assert.equal(cc, pages.length);
+ for (var i = 0; i < cc; i++) {
+ var node = dayContainer.getChild(i);
+ Assert.equal(pages[i], node.uri);
+ }
+
+ dayContainer.containerOpen = false;
+ root.containerOpen = false;
+});
+
+/**
+ * Tests that sorting date query by date will sort accordingly.
+ */
+add_task(function () {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_QUERY;
+ // This should sort by title asc.
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var dayContainer = root
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayContainer.containerOpen = true;
+
+ var cc = dayContainer.childCount;
+ Assert.equal(cc, pages.length);
+ for (var i = 0; i < cc; i++) {
+ var node = dayContainer.getChild(i);
+ Assert.equal(pages[pages.length - i - 1], node.uri);
+ }
+
+ dayContainer.containerOpen = false;
+ root.containerOpen = false;
+});
+
+/**
+ * Tests that sorting date site query by date will still sort by title asc.
+ */
+add_task(function () {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ // This should sort by title asc.
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var dayContainer = root
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayContainer.containerOpen = true;
+ var siteContainer = dayContainer
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ Assert.equal(siteContainer.title, "a.mozilla.org");
+ siteContainer.containerOpen = true;
+
+ var cc = siteContainer.childCount;
+ Assert.equal(cc, pages.length / 2);
+ for (var i = 0; i < cc / 2; i++) {
+ var node = siteContainer.getChild(i);
+ Assert.equal(pages[i], node.uri);
+ }
+
+ siteContainer.containerOpen = false;
+ dayContainer.containerOpen = false;
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_536081.js b/toolkit/components/places/tests/unit/test_536081.js
new file mode 100644
index 0000000000..4105b09d1b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_536081.js
@@ -0,0 +1,35 @@
+/* -*- 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/. */
+
+const TEST_URL = {
+ u: "http://www.google.com/search?q=testing%3Bthis&ie=utf-8&oe=utf-8&aq=t&rls=org.mozilla:en-US:unofficial&client=firefox-a",
+ s: "goog",
+};
+
+add_task(async function () {
+ print("Testing url: " + TEST_URL.u);
+ await PlacesTestUtils.addVisits(uri(TEST_URL.u));
+
+ let query = PlacesUtils.history.getNewQuery();
+ query.searchTerms = TEST_URL.s;
+ let options = PlacesUtils.history.getNewQueryOptions();
+
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ Assert.equal(cc, 1);
+
+ print("Checking url is in the query.");
+ let node = root.getChild(0);
+ print("Found " + node.uri);
+
+ Assert.ok(await PlacesTestUtils.isPageInDB(TEST_URL.u));
+
+ root.containerOpen = false;
+ await PlacesUtils.history.remove(node.uri);
+
+ Assert.equal(false, await PlacesTestUtils.isPageInDB(TEST_URL.u));
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesDBUtils_removeOldCorruptDBs.js b/toolkit/components/places/tests/unit/test_PlacesDBUtils_removeOldCorruptDBs.js
new file mode 100644
index 0000000000..26ec85ffc4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesDBUtils_removeOldCorruptDBs.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEMP_FILES_TO_CREATE = 5;
+const LAST_MODIFICATION_DAY = [5, 10, 15, 20, 25];
+const TEST_CURRENT_TIME = Date.now();
+const MS_PER_DAY = 86400000;
+const RETAIN_DAYS = 14;
+
+async function createfiles() {
+ for (let i = 0; i < TEMP_FILES_TO_CREATE; i++) {
+ let setTime = TEST_CURRENT_TIME;
+ setTime -= LAST_MODIFICATION_DAY[i] * MS_PER_DAY;
+ let fileName = "places.sqlite" + (i > 0 ? "-" + i : "") + ".corrupt";
+ let filePath = PathUtils.join(PathUtils.profileDir, fileName);
+ await IOUtils.writeUTF8(filePath, "test-file-delete-me", {
+ tmpPath: filePath + ".tmp",
+ });
+ Assert.ok(await IOUtils.exists(filePath), "file created: " + filePath);
+ await IOUtils.setModificationTime(filePath, setTime);
+ }
+}
+
+add_task(async function removefiles() {
+ await createfiles();
+ await PlacesDBUtils.runTasks([PlacesDBUtils.removeOldCorruptDBs]);
+ for (let i = 0; i < TEMP_FILES_TO_CREATE; i++) {
+ let fileName = "places.sqlite" + (i > 0 ? "-" + i : "") + ".corrupt";
+ let filePath = PathUtils.join(PathUtils.profileDir, fileName);
+ if (LAST_MODIFICATION_DAY[i] >= RETAIN_DAYS) {
+ Assert.ok(
+ !(await IOUtils.exists(filePath)),
+ "Old corrupt file has been removed" + filePath
+ );
+ } else {
+ Assert.ok(
+ await IOUtils.exists(filePath),
+ "Files that are not old are not removed" + filePath
+ );
+ }
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesQuery_history.js b/toolkit/components/places/tests/unit/test_PlacesQuery_history.js
new file mode 100644
index 0000000000..9546e03730
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesQuery_history.js
@@ -0,0 +1,245 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { PlacesQuery } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesQuery.sys.mjs"
+);
+
+const placesQuery = new PlacesQuery();
+
+registerCleanupFunction(() => placesQuery.close());
+
+async function waitForUpdateHistoryTask(updateTask) {
+ const promise = new Promise(resolve =>
+ placesQuery.observeHistory(newHistory => resolve(newHistory))
+ );
+ await updateTask();
+ return promise;
+}
+
+add_task(async function test_visits_cache_is_updated() {
+ const now = new Date();
+ info("Insert the first visit.");
+ await PlacesUtils.history.insert({
+ url: "https://www.example.com/",
+ title: "Example Domain",
+ visits: [{ date: now }],
+ });
+ let history = await placesQuery.getHistory();
+ const mapKey = placesQuery.getStartOfDayTimestamp(now);
+ history = history.get(mapKey);
+ Assert.equal(history.length, 1);
+ Assert.equal(history[0].url, "https://www.example.com/");
+ Assert.equal(history[0].date.getTime(), now.getTime());
+ Assert.equal(history[0].title, "Example Domain");
+
+ info("Insert the next visit.");
+ history = await waitForUpdateHistoryTask(() =>
+ PlacesUtils.history.insert({
+ url: "https://example.net/",
+ visits: [{ date: now }],
+ })
+ );
+ history = history.get(mapKey);
+ Assert.equal(history.length, 2);
+ Assert.equal(
+ history[0].url,
+ "https://example.net/",
+ "The most recent visit should come first."
+ );
+ Assert.equal(history[0].date.getTime(), now.getTime());
+
+ info("Remove the first visit.");
+ history = await waitForUpdateHistoryTask(() =>
+ PlacesUtils.history.remove("https://www.example.com/")
+ );
+ history = history.get(mapKey);
+ Assert.equal(history.length, 1);
+ Assert.equal(history[0].url, "https://example.net/");
+
+ info("Remove all visits.");
+ history = await waitForUpdateHistoryTask(() => PlacesUtils.history.clear());
+ Assert.equal(history.size, 0);
+});
+
+add_task(async function test_filter_visits_by_age() {
+ const now = new Date();
+ await PlacesUtils.history.insertMany([
+ {
+ url: "https://www.example.com/",
+ visits: [{ date: new Date("2000-01-01T12:00:00") }],
+ },
+ {
+ url: "https://example.net/",
+ visits: [{ date: now }],
+ },
+ ]);
+ let history = await placesQuery.getHistory({ daysOld: 1 });
+ Assert.equal(history.size, 1, "The older visit should be excluded.");
+ Assert.equal(
+ history.get(placesQuery.getStartOfDayTimestamp(now))[0].url,
+ "https://example.net/"
+ );
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_visits_sort_option() {
+ const now = new Date();
+ info("Get history sorted by site.");
+ await PlacesUtils.history.insertMany([
+ { url: "https://www.reddit.com/", visits: [{ date: now }] },
+ { url: "https://twitter.com/", visits: [{ date: now }] },
+ ]);
+ let history = await placesQuery.getHistory({ sortBy: "site" });
+ ["reddit.com", "twitter.com"].forEach(domain =>
+ Assert.ok(history.has(domain))
+ );
+
+ info("Insert the next visit.");
+ history = await waitForUpdateHistoryTask(() =>
+ PlacesUtils.history.insert({
+ url: "https://www.wikipedia.org/",
+ visits: [{ date: now }],
+ })
+ );
+ ["reddit.com", "twitter.com", "wikipedia.org"].forEach(domain =>
+ Assert.ok(history.has(domain))
+ );
+
+ info("Update the sort order.");
+ history = await placesQuery.getHistory({ sortBy: "date" });
+ const mapKey = placesQuery.getStartOfDayTimestamp(now);
+ Assert.equal(history.get(mapKey).length, 3);
+
+ info("Insert the next visit.");
+ history = await waitForUpdateHistoryTask(() =>
+ PlacesUtils.history.insert({
+ url: "https://www.youtube.com/",
+ visits: [{ date: now }],
+ })
+ );
+ Assert.equal(history.get(mapKey).length, 4);
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_visits_limit_option() {
+ await PlacesUtils.history.insertMany([
+ {
+ url: "https://www.example.com/",
+ visits: [{ date: new Date() }],
+ },
+ {
+ url: "https://example.net/",
+ visits: [{ date: new Date() }],
+ },
+ ]);
+ let history = await placesQuery.getHistory({ limit: 1 });
+ Assert.equal(
+ [...history.values()].reduce((acc, { length }) => acc + length, 0),
+ 1,
+ "Number of visits should be limited to 1."
+ );
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_dedupe_visits_by_url() {
+ const today = new Date();
+ const yesterday = new Date(
+ today.getFullYear(),
+ today.getMonth(),
+ today.getDate() - 1
+ );
+ await PlacesUtils.history.insertMany([
+ {
+ url: "https://www.example.com/",
+ visits: [{ date: yesterday }],
+ },
+ {
+ url: "https://www.example.com/",
+ visits: [{ date: today }],
+ },
+ {
+ url: "https://www.example.com/",
+ visits: [{ date: today }],
+ },
+ ]);
+ info("Get history sorted by date.");
+ let history = await placesQuery.getHistory({ sortBy: "date" });
+ Assert.equal(
+ history.get(placesQuery.getStartOfDayTimestamp(yesterday)).length,
+ 1,
+ "There was only one visit from yesterday."
+ );
+ Assert.equal(
+ history.get(placesQuery.getStartOfDayTimestamp(today)).length,
+ 1,
+ "The duplicate visit from today should be removed."
+ );
+ history = await waitForUpdateHistoryTask(() =>
+ PlacesUtils.history.insert({
+ url: "https://www.example.com/",
+ visits: [{ date: today }],
+ })
+ );
+ Assert.equal(
+ history.get(placesQuery.getStartOfDayTimestamp(today)).length,
+ 1,
+ "Visits inserted from `page-visited` events should be deduped."
+ );
+
+ info("Get history sorted by site.");
+ history = await placesQuery.getHistory({ sortBy: "site" });
+ Assert.equal(
+ history.get("example.com").length,
+ 1,
+ "The duplicate visits for this site should be removed."
+ );
+ history = await waitForUpdateHistoryTask(() =>
+ PlacesUtils.history.insert({
+ url: "https://www.example.com/",
+ visits: [{ date: yesterday }],
+ })
+ );
+ const visits = history.get("example.com");
+ Assert.equal(
+ visits.length,
+ 1,
+ "Visits inserted from `page-visited` events should be deduped."
+ );
+ Assert.equal(
+ visits[0].date.getTime(),
+ today.getTime(),
+ "Deduping keeps the most recent visit."
+ );
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_search_visits() {
+ const now = new Date();
+ await PlacesUtils.history.insertMany([
+ {
+ url: "https://www.example.com/",
+ title: "First Visit",
+ visits: [{ date: now }],
+ },
+ {
+ url: "https://example.net/",
+ title: "Second Visit",
+ visits: [{ date: now }],
+ },
+ ]);
+
+ let results = await placesQuery.searchHistory("Visit");
+ Assert.equal(results.length, 2, "Both visits match the search query.");
+
+ results = await placesQuery.searchHistory("First Visit");
+ Assert.equal(results.length, 1, "One visit matches the search query.");
+
+ results = await placesQuery.searchHistory("Bogus");
+ Assert.equal(results.length, 0, "Neither visit matches the search query.");
+
+ await PlacesUtils.history.clear();
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_isRootItem.js b/toolkit/components/places/tests/unit/test_PlacesUtils_isRootItem.js
new file mode 100644
index 0000000000..f626d437e0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_isRootItem.js
@@ -0,0 +1,21 @@
+"use strict";
+
+const GUIDS = [
+ PlacesUtils.bookmarks.rootGuid,
+ ...PlacesUtils.bookmarks.userContentRoots,
+ PlacesUtils.bookmarks.tagsGuid,
+];
+
+add_task(async function test_isRootItem() {
+ for (let guid of GUIDS) {
+ Assert.ok(
+ PlacesUtils.isRootItem(guid),
+ `Should correctly identify root item ${guid}`
+ );
+ }
+
+ Assert.ok(
+ !PlacesUtils.isRootItem("fakeguid1234"),
+ "Should not identify other items as root."
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_unwrapNodes_place.js b/toolkit/components/places/tests/unit/test_PlacesUtils_unwrapNodes_place.js
new file mode 100644
index 0000000000..8944dc22f4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_unwrapNodes_place.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that unwrapNodes properly filters out place: uris from text flavors.
+
+add_task(function () {
+ let tests = [
+ // Single url.
+ ["place:type=0&sort=1:", PlacesUtils.TYPE_X_MOZ_URL],
+ // Multiple urls.
+ [
+ "place:type=0&sort=1:\nfirst\nplace:type=0&sort=1\nsecond",
+ PlacesUtils.TYPE_X_MOZ_URL,
+ ],
+ // Url == title.
+ ["place:type=0&sort=1:\nplace:type=0&sort=1", PlacesUtils.TYPE_X_MOZ_URL],
+ // Malformed.
+ [
+ "place:type=0&sort=1:\nplace:type=0&sort=1\nmalformed",
+ PlacesUtils.TYPE_X_MOZ_URL,
+ ],
+ // Single url.
+ ["place:type=0&sort=1:", PlacesUtils.TYPE_PLAINTEXT],
+ // Multiple urls.
+ ["place:type=0&sort=1:\nplace:type=0&sort=1", PlacesUtils.TYPE_PLAINTEXT],
+ ];
+ for (let [blob, type] of tests) {
+ Assert.deepEqual(
+ PlacesUtils.unwrapNodes(blob, type),
+ [],
+ "No valid entries should be found"
+ );
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js
new file mode 100644
index 0000000000..084415fb37
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This is a test for asyncExecuteLegacyQuery API.
+
+add_task(async function test_history_query() {
+ let uri = "http://test.visit.mozilla.com/";
+ let title = "Test visit";
+ await PlacesTestUtils.addVisits({ uri, title });
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ let query = PlacesUtils.history.getNewQuery();
+
+ return new Promise(resolve => {
+ PlacesUtils.history.asyncExecuteLegacyQuery(query, options, {
+ handleResult(aResultSet) {
+ for (let row; (row = aResultSet.getNextRow()); ) {
+ try {
+ Assert.equal(row.getResultByIndex(1), uri);
+ Assert.equal(row.getResultByIndex(2), title);
+ } catch (e) {
+ do_throw("Error while fetching page data.");
+ }
+ }
+ },
+ handleError(aError) {
+ do_throw(
+ "Async execution error (" + aError.result + "): " + aError.message
+ );
+ },
+ handleCompletion(aReason) {
+ cleanupTest().then(resolve);
+ },
+ });
+ });
+});
+
+add_task(async function test_bookmarks_query() {
+ let url = "http://test.bookmark.mozilla.com/";
+ let title = "Test bookmark";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title,
+ url,
+ });
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode =
+ Ci.nsINavHistoryQueryOptions.SORT_BY_LASTMODIFIED_DESCENDING;
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let query = PlacesUtils.history.getNewQuery();
+
+ return new Promise(resolve => {
+ PlacesUtils.history.asyncExecuteLegacyQuery(query, options, {
+ handleResult(aResultSet) {
+ for (let row; (row = aResultSet.getNextRow()); ) {
+ try {
+ Assert.equal(row.getResultByIndex(1), url);
+ Assert.equal(row.getResultByIndex(2), title);
+ } catch (e) {
+ do_throw("Error while fetching page data.");
+ }
+ }
+ },
+ handleError(aError) {
+ do_throw(
+ "Async execution error (" + aError.result + "): " + aError.message
+ );
+ },
+ handleCompletion(aReason) {
+ cleanupTest().then(resolve);
+ },
+ });
+ });
+});
+
+function cleanupTest() {
+ return Promise.all([
+ PlacesUtils.history.clear(),
+ PlacesUtils.bookmarks.eraseEverything(),
+ ]);
+}
diff --git a/toolkit/components/places/tests/unit/test_async_transactions.js b/toolkit/components/places/tests/unit/test_async_transactions.js
new file mode 100644
index 0000000000..b0e9b292f3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -0,0 +1,2125 @@
+/* -*- 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/. */
+
+const bmsvc = PlacesUtils.bookmarks;
+const obsvc = PlacesUtils.observers;
+const tagssvc = PlacesUtils.tagging;
+const PT = PlacesTransactions;
+const menuGuid = PlacesUtils.bookmarks.menuGuid;
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+// Create and add bookmarks observer.
+var observer = {
+ tagRelatedGuids: new Set(),
+
+ reset() {
+ this.itemsAdded = new Map();
+ this.itemsRemoved = new Map();
+ this.itemsMoved = new Map();
+ this.itemsKeywordChanged = new Map();
+ this.itemsTitleChanged = new Map();
+ this.itemsUrlChanged = new Map();
+ },
+
+ handlePlacesEvents(events) {
+ for (let event of events) {
+ switch (event.type) {
+ case "bookmark-added":
+ // Ignore tag items.
+ if (event.isTagging) {
+ this.tagRelatedGuids.add(event.guid);
+ return;
+ }
+
+ this.itemsAdded.set(event.guid, {
+ itemId: event.id,
+ parentGuid: event.parentGuid,
+ index: event.index,
+ itemType: event.itemType,
+ title: event.title,
+ url: event.url,
+ });
+ break;
+ case "bookmark-removed":
+ if (this.tagRelatedGuids.has(event.guid)) {
+ return;
+ }
+
+ this.itemsRemoved.set(event.guid, {
+ parentGuid: event.parentGuid,
+ index: event.index,
+ itemType: event.itemType,
+ });
+ break;
+ case "bookmark-moved":
+ this.itemsMoved.set(event.guid, {
+ oldParentGuid: event.oldParentGuid,
+ oldIndex: event.oldIndex,
+ newParentGuid: event.parentGuid,
+ newIndex: event.index,
+ itemType: event.itemType,
+ });
+ break;
+ case "bookmark-keyword-changed":
+ if (this.tagRelatedGuids.has(event.guid)) {
+ return;
+ }
+
+ this.itemsKeywordChanged.set(event.guid, {
+ keyword: event.keyword,
+ });
+ break;
+ case "bookmark-title-changed":
+ if (this.tagRelatedGuids.has(event.guid)) {
+ return;
+ }
+
+ this.itemsTitleChanged.set(event.guid, {
+ title: event.title,
+ parentGuid: event.parentGuid,
+ });
+ break;
+ case "bookmark-url-changed":
+ if (this.tagRelatedGuids.has(event.guid)) {
+ return;
+ }
+
+ this.itemsUrlChanged.set(event.guid, {
+ url: event.url,
+ });
+ break;
+ }
+ }
+ },
+};
+observer.reset();
+
+// index at which items should begin
+var bmStartIndex = 0;
+
+function run_test() {
+ observer.handlePlacesEvents = observer.handlePlacesEvents.bind(observer);
+ obsvc.addListener(
+ [
+ "bookmark-added",
+ "bookmark-removed",
+ "bookmark-moved",
+ "bookmark-keyword-changed",
+ "bookmark-title-changed",
+ "bookmark-url-changed",
+ ],
+ observer.handlePlacesEvents
+ );
+ registerCleanupFunction(function () {
+ obsvc.removeListener(
+ [
+ "bookmark-added",
+ "bookmark-removed",
+ "bookmark-moved",
+ "bookmark-keyword-changed",
+ "bookmark-title-changed",
+ "bookmark-url-changed",
+ ],
+ observer.handlePlacesEvents
+ );
+ });
+
+ run_next_test();
+}
+
+function sanityCheckTransactionHistory() {
+ Assert.ok(PT.undoPosition <= PT.length);
+
+ let check_entry_throws = f => {
+ try {
+ f();
+ do_throw("PT.entry should throw for invalid input");
+ } catch (ex) {}
+ };
+ check_entry_throws(() => PT.entry(-1));
+ check_entry_throws(() => PT.entry({}));
+ check_entry_throws(() => PT.entry(PT.length));
+
+ if (PT.undoPosition < PT.length) {
+ Assert.equal(PT.topUndoEntry, PT.entry(PT.undoPosition));
+ } else {
+ Assert.equal(null, PT.topUndoEntry);
+ }
+ if (PT.undoPosition > 0) {
+ Assert.equal(PT.topRedoEntry, PT.entry(PT.undoPosition - 1));
+ } else {
+ Assert.equal(null, PT.topRedoEntry);
+ }
+}
+
+function getTransactionsHistoryState() {
+ let history = [];
+ for (let i = 0; i < PT.length; i++) {
+ history.push(PT.entry(i));
+ }
+ return [history, PT.undoPosition];
+}
+
+function ensureUndoState(aExpectedEntries = [], aExpectedUndoPosition = 0) {
+ // ensureUndoState is called in various places during this test, so it's
+ // a good places to sanity-check the transaction-history APIs in all
+ // cases.
+ sanityCheckTransactionHistory();
+
+ let [actualEntries, actualUndoPosition] = getTransactionsHistoryState();
+ Assert.equal(actualEntries.length, aExpectedEntries.length);
+ Assert.equal(actualUndoPosition, aExpectedUndoPosition);
+
+ function checkEqualEntries(aExpectedEntry, aActualEntry) {
+ Assert.equal(aExpectedEntry.length, aActualEntry.length);
+ aExpectedEntry.forEach((t, i) => Assert.equal(t, aActualEntry[i]));
+ }
+ aExpectedEntries.forEach((e, i) => checkEqualEntries(e, actualEntries[i]));
+}
+
+function ensureItemsAdded(...items) {
+ let expectedResultsCount = items.length;
+
+ for (let item of items) {
+ if ("children" in item) {
+ expectedResultsCount += item.children.length;
+ }
+ Assert.ok(
+ observer.itemsAdded.has(item.guid),
+ `Should have the expected guid ${item.guid}`
+ );
+ let info = observer.itemsAdded.get(item.guid);
+ Assert.equal(
+ info.parentGuid,
+ item.parentGuid,
+ "Should have notified the correct parentGuid"
+ );
+ for (let propName of ["title", "index", "itemType"]) {
+ if (propName in item) {
+ Assert.equal(info[propName], item[propName]);
+ }
+ }
+ if ("url" in item) {
+ Assert.ok(
+ Services.io.newURI(info.url).equals(Services.io.newURI(item.url)),
+ "Should have the correct url"
+ );
+ }
+ }
+
+ Assert.equal(
+ observer.itemsAdded.size,
+ expectedResultsCount,
+ "Should have added the correct number of items"
+ );
+}
+
+function ensureItemsRemoved(...items) {
+ let expectedResultsCount = items.length;
+
+ for (let item of items) {
+ // We accept both guids and full info object here.
+ if (typeof item == "string") {
+ Assert.ok(
+ observer.itemsRemoved.has(item),
+ `Should have removed the expected guid ${item}`
+ );
+ } else {
+ if ("children" in item) {
+ expectedResultsCount += item.children.length;
+ }
+
+ Assert.ok(
+ observer.itemsRemoved.has(item.guid),
+ `Should have removed expected guid ${item.guid}`
+ );
+ let info = observer.itemsRemoved.get(item.guid);
+ Assert.equal(
+ info.parentGuid,
+ item.parentGuid,
+ "Should have notified the correct parentGuid"
+ );
+ if ("index" in item) {
+ Assert.equal(info.index, item.index);
+ }
+ }
+ }
+
+ Assert.equal(
+ observer.itemsRemoved.size,
+ expectedResultsCount,
+ "Should have removed the correct number of items"
+ );
+}
+
+function ensureItemsMoved(...items) {
+ Assert.equal(
+ observer.itemsMoved.size,
+ items.length,
+ "Should have received the correct number of moved notifications"
+ );
+ for (let item of items) {
+ Assert.ok(
+ observer.itemsMoved.has(item.guid),
+ `Observer should have a move for ${item.guid}`
+ );
+ let info = observer.itemsMoved.get(item.guid);
+ Assert.equal(
+ info.oldParentGuid,
+ item.oldParentGuid,
+ "Should have the correct old parent guid"
+ );
+ Assert.equal(
+ info.oldIndex,
+ item.oldIndex,
+ "Should have the correct old index"
+ );
+ Assert.equal(
+ info.newParentGuid,
+ item.newParentGuid,
+ "Should have the correct new parent guid"
+ );
+ Assert.equal(
+ info.newIndex,
+ item.newIndex,
+ "Should have the correct new index"
+ );
+ }
+}
+
+function ensureItemsKeywordChanged(...items) {
+ for (const item of items) {
+ Assert.ok(
+ observer.itemsKeywordChanged.has(item.guid),
+ `Observer should have a keyword changed for ${item.guid}`
+ );
+ const info = observer.itemsKeywordChanged.get(item.guid);
+ Assert.equal(info.keyword, item.keyword, "Should have the correct keyword");
+ }
+}
+
+function ensureItemsTitleChanged(...items) {
+ Assert.equal(
+ observer.itemsTitleChanged.size,
+ items.length,
+ "Should have received the correct number of bookmark-title-changed notifications"
+ );
+ for (const item of items) {
+ Assert.ok(
+ observer.itemsTitleChanged.has(item.guid),
+ `Observer should have a title changed for ${item.guid}`
+ );
+ const info = observer.itemsTitleChanged.get(item.guid);
+ Assert.equal(info.title, item.title, "Should have the correct title");
+ Assert.equal(
+ info.parentGuid,
+ item.parentGuid,
+ "Should have the correct parent guid"
+ );
+ }
+}
+
+function ensureItemsUrlChanged(...items) {
+ Assert.equal(
+ observer.itemsUrlChanged.size,
+ items.length,
+ "Should have received the correct number of bookmark-url-changed notifications"
+ );
+ for (const item of items) {
+ Assert.ok(
+ observer.itemsUrlChanged.has(item.guid),
+ `Observer should have a title changed for ${item.guid}`
+ );
+ const info = observer.itemsUrlChanged.get(item.guid);
+ Assert.equal(info.url, item.url, "Should have the correct url");
+ }
+}
+
+function ensureTagsForURI(aURI, aTags) {
+ let tagsSet = tagssvc.getTagsForURI(Services.io.newURI(aURI));
+ Assert.equal(tagsSet.length, aTags.length);
+ Assert.ok(aTags.every(t => tagsSet.includes(t)));
+}
+
+function createTestFolderInfo(
+ title = "Test Folder",
+ parentGuid = menuGuid,
+ children = undefined
+) {
+ let info = { parentGuid, title };
+ if (children) {
+ info.children = children;
+ }
+ return info;
+}
+
+function removeAllDatesInTree(tree) {
+ if ("lastModified" in tree) {
+ delete tree.lastModified;
+ }
+ if ("dateAdded" in tree) {
+ delete tree.dateAdded;
+ }
+
+ if (!tree.children) {
+ return;
+ }
+
+ for (let child of tree.children) {
+ removeAllDatesInTree(child);
+ }
+}
+
+// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the
+// same.
+// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set.
+async function ensureEqualBookmarksTrees(
+ aOriginal,
+ aNew,
+ aIsRestoredItem = true,
+ aCheckParentAndPosition = false,
+ aIgnoreAllDates = false
+) {
+ // Note "id" is not-enumerable, and is therefore skipped by Object.keys (both
+ // ours and the one at deepEqual). This is fine for us because ids are not
+ // restored by Redo.
+ if (aIsRestoredItem) {
+ if (aIgnoreAllDates) {
+ removeAllDatesInTree(aOriginal);
+ removeAllDatesInTree(aNew);
+ } else if (!aOriginal.lastModified) {
+ // Ignore lastModified for newly created items, for performance reasons.
+ aNew.lastModified = aOriginal.lastModified;
+ }
+ Assert.deepEqual(aOriginal, aNew);
+ return;
+ }
+
+ for (let property of Object.keys(aOriginal)) {
+ if (property == "children") {
+ Assert.equal(aOriginal.children.length, aNew.children.length);
+ for (let i = 0; i < aOriginal.children.length; i++) {
+ await ensureEqualBookmarksTrees(
+ aOriginal.children[i],
+ aNew.children[i],
+ false,
+ true,
+ aIgnoreAllDates
+ );
+ }
+ } else if (property == "guid") {
+ // guid shouldn't be copied if the item was not restored.
+ Assert.notEqual(aOriginal.guid, aNew.guid);
+ } else if (property == "dateAdded") {
+ // dateAdded shouldn't be copied if the item was not restored.
+ Assert.ok(is_time_ordered(aOriginal.dateAdded, aNew.dateAdded));
+ } else if (property == "lastModified") {
+ // same same, except for the never-changed case
+ if (!aOriginal.lastModified) {
+ Assert.ok(!aNew.lastModified);
+ } else {
+ Assert.ok(is_time_ordered(aOriginal.lastModified, aNew.lastModified));
+ }
+ } else if (
+ aCheckParentAndPosition ||
+ (property != "parentGuid" && property != "index")
+ ) {
+ Assert.deepEqual(aOriginal[property], aNew[property]);
+ }
+ }
+}
+
+async function ensureBookmarksTreeRestoredCorrectly(
+ ...aOriginalBookmarksTrees
+) {
+ for (let originalTree of aOriginalBookmarksTrees) {
+ let restoredTree = await PlacesUtils.promiseBookmarksTree(
+ originalTree.guid
+ );
+ await ensureEqualBookmarksTrees(originalTree, restoredTree);
+ }
+}
+
+async function ensureBookmarksTreeRestoredCorrectlyExceptDates(
+ ...aOriginalBookmarksTrees
+) {
+ for (let originalTree of aOriginalBookmarksTrees) {
+ let restoredTree = await PlacesUtils.promiseBookmarksTree(
+ originalTree.guid
+ );
+ await ensureEqualBookmarksTrees(
+ originalTree,
+ restoredTree,
+ true,
+ false,
+ true
+ );
+ }
+}
+
+async function ensureNonExistent(...aGuids) {
+ for (let guid of aGuids) {
+ Assert.strictEqual(await PlacesUtils.promiseBookmarksTree(guid), null);
+ }
+}
+
+add_task(async function test_recycled_transactions() {
+ async function ensureTransactThrowsFor(aTransaction) {
+ let [txns, undoPosition] = getTransactionsHistoryState();
+ try {
+ await aTransaction.transact();
+ do_throw("Shouldn't be able to use the same transaction twice");
+ } catch (ex) {}
+ ensureUndoState(txns, undoPosition);
+ }
+
+ let txn_a = PT.NewFolder(createTestFolderInfo());
+ await txn_a.transact();
+ ensureUndoState([[txn_a]], 0);
+ await ensureTransactThrowsFor(txn_a);
+
+ await PT.undo();
+ ensureUndoState([[txn_a]], 1);
+ ensureTransactThrowsFor(txn_a);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+ ensureTransactThrowsFor(txn_a);
+
+ let txn_b = PT.NewFolder(createTestFolderInfo());
+ let results = await PT.batch([
+ txn_a, // Shouldn't be able to use the same transaction twice.
+ txn_b,
+ ]);
+ Assert.strictEqual(
+ results[0],
+ undefined,
+ "First transaction should fail as it can't be reused"
+ );
+ ensureUndoState([[txn_b]], 0);
+
+ await PT.undo();
+ ensureUndoState([[txn_b]], 1);
+ ensureTransactThrowsFor(txn_a);
+ ensureTransactThrowsFor(txn_b);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+ observer.reset();
+});
+
+add_task(async function test_new_folder_with_children() {
+ let folder_info = createTestFolderInfo(
+ "Test folder",
+ PlacesUtils.bookmarks.menuGuid,
+ [
+ {
+ url: "http://test_create_item.com",
+ title: "Test creating an item",
+ },
+ ]
+ );
+ ensureUndoState();
+ let txn = PT.NewFolder(folder_info);
+ folder_info.guid = await txn.transact();
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(folder_info.guid);
+ let ensureDo = async function (aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ ensureItemsAdded(folder_info);
+ if (aRedo) {
+ // Ignore lastModified in the comparison, for performance reasons.
+ originalInfo.lastModified = null;
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo);
+ }
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({
+ guid: folder_info.guid,
+ parentGuid: folder_info.parentGuid,
+ index: bmStartIndex,
+ children: [
+ {
+ title: "Test creating an item",
+ url: "http://test_create_item.com",
+ },
+ ],
+ });
+ observer.reset();
+ };
+
+ await ensureDo();
+ await PT.undo();
+ await ensureUndo();
+ await PT.redo();
+ await ensureDo(true);
+ await PT.undo();
+ ensureUndo();
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_new_bookmark() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test_create_item.com",
+ index: bmStartIndex,
+ title: "Test creating an item",
+ };
+
+ ensureUndoState();
+ let txn = PT.NewBookmark(bm_info);
+ bm_info.guid = await txn.transact();
+
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(bm_info.guid);
+ let ensureDo = async function (aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ await ensureItemsAdded(bm_info);
+ if (aRedo) {
+ await ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ }
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({
+ guid: bm_info.guid,
+ parentGuid: bm_info.parentGuid,
+ index: bmStartIndex,
+ });
+ observer.reset();
+ };
+
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo(true);
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_merge_create_folder_and_item() {
+ let folder_info = createTestFolderInfo();
+ let bm_info = {
+ url: "http://test_create_item_to_folder.com",
+ title: "Test Bookmark",
+ index: bmStartIndex,
+ };
+
+ let folderTxn = PT.NewFolder(folder_info);
+ let bmTxn;
+ let results = await PT.batch([
+ folderTxn,
+ previousResults => {
+ bm_info.parentGuid = previousResults[0];
+ return (bmTxn = PT.NewBookmark(bm_info));
+ },
+ ]);
+ folder_info.guid = results[0];
+ bm_info.guid = results[1];
+
+ let ensureDo = async function () {
+ ensureUndoState([[bmTxn, folderTxn]], 0);
+ await ensureItemsAdded(folder_info, bm_info);
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[bmTxn, folderTxn]], 1);
+ ensureItemsRemoved(folder_info, bm_info);
+ observer.reset();
+ };
+
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ await ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_move_items_to_folder() {
+ let folder_a_info = createTestFolderInfo("Folder A");
+ let bm_a_info = { url: "http://test_move_items.com", title: "Bookmark A" };
+ let bm_b_info = { url: "http://test_move_items.com", title: "Bookmark B" };
+
+ // Test moving items within the same folder.
+ let folder_a_txn = PT.NewFolder(folder_a_info);
+ let bm_a_txn, bm_b_txn;
+ let results = await PT.batch([
+ folder_a_txn,
+ previousResults => {
+ bm_a_info.parentGuid = previousResults[0];
+ return (bm_a_txn = PT.NewBookmark(bm_a_info));
+ },
+ previousResults => {
+ bm_b_info.parentGuid = previousResults[0];
+ return (bm_b_txn = PT.NewBookmark(bm_b_info));
+ },
+ ]);
+ console.log("results: " + results);
+ folder_a_info.guid = results[0];
+ bm_a_info.guid = results[1];
+ bm_b_info.guid = results[2];
+
+ ensureUndoState([[bm_b_txn, bm_a_txn, folder_a_txn]], 0);
+
+ let moveTxn = PT.Move({
+ guid: bm_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ });
+ await moveTxn.transact();
+
+ let ensureDo = () => {
+ ensureUndoState([[moveTxn], [bm_b_txn, bm_a_txn, folder_a_txn]], 0);
+ ensureItemsMoved({
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 1,
+ });
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState([[moveTxn], [bm_b_txn, bm_a_txn, folder_a_txn]], 1);
+ ensureItemsMoved({
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 1,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory(false, true);
+ ensureUndoState([[bm_b_txn, bm_a_txn, folder_a_txn]], 0);
+
+ // Test moving items between folders.
+ let folder_b_info = createTestFolderInfo("Folder B");
+ let folder_b_txn = PT.NewFolder(folder_b_info);
+ folder_b_info.guid = await folder_b_txn.transact();
+ ensureUndoState([[folder_b_txn], [bm_b_txn, bm_a_txn, folder_a_txn]], 0);
+
+ moveTxn = PT.Move({
+ guid: bm_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ newIndex: bmsvc.DEFAULT_INDEX,
+ });
+ await moveTxn.transact();
+
+ ensureDo = () => {
+ ensureUndoState(
+ [[moveTxn], [folder_b_txn], [bm_b_txn, bm_a_txn, folder_a_txn]],
+ 0
+ );
+ ensureItemsMoved({
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+ ensureUndo = () => {
+ ensureUndoState(
+ [[moveTxn], [folder_b_txn], [bm_b_txn, bm_a_txn, folder_a_txn]],
+ 1
+ );
+ ensureItemsMoved({
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_b_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ // Clean up
+ await PT.undo(); // folder_b_txn
+ await PT.undo(); // folder_a_txn + the bookmarks;
+ Assert.equal(observer.itemsRemoved.size, 4);
+ ensureUndoState(
+ [[moveTxn], [folder_b_txn], [bm_b_txn, bm_a_txn, folder_a_txn]],
+ 3
+ );
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_move_multiple_items_to_folder() {
+ let folder_a_info = createTestFolderInfo("Folder A");
+ let bm_a_info = { url: "http://test_move_items.com", title: "Bookmark A" };
+ let bm_b_info = { url: "http://test_move_items.com", title: "Bookmark B" };
+ let bm_c_info = { url: "http://test_move_items.com", title: "Bookmark C" };
+
+ // Test moving items within the same folder.
+
+ let folder_a_txn = PT.NewFolder(folder_a_info);
+ let bm_a_txn, bm_b_txn, bm_c_txn;
+ let results = await PT.batch([
+ folder_a_txn,
+ previousResults => {
+ bm_a_info.parentGuid = previousResults[0];
+ return (bm_a_txn = PT.NewBookmark(bm_a_info));
+ },
+ previousResults => {
+ bm_b_info.parentGuid = previousResults[0];
+ return (bm_b_txn = PT.NewBookmark(bm_b_info));
+ },
+ previousResults => {
+ bm_c_info.parentGuid = previousResults[0];
+ return (bm_c_txn = PT.NewBookmark(bm_c_info));
+ },
+ ]);
+
+ folder_a_info.guid = results[0];
+ bm_a_info.guid = results[1];
+ bm_b_info.guid = results[2];
+ bm_c_info.guid = results[3];
+
+ ensureUndoState([[bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]], 0);
+
+ let moveTxn = PT.Move({
+ guids: [bm_a_info.guid, bm_b_info.guid],
+ newParentGuid: folder_a_info.guid,
+ });
+ await moveTxn.transact();
+
+ let ensureDo = () => {
+ ensureUndoState(
+ [[moveTxn], [bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]],
+ 0
+ );
+ ensureItemsMoved(
+ {
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 2,
+ },
+ {
+ guid: bm_b_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 1,
+ newIndex: 2,
+ }
+ );
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState(
+ [[moveTxn], [bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]],
+ 1
+ );
+ ensureItemsMoved(
+ {
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 1,
+ newIndex: 0,
+ },
+ {
+ guid: bm_b_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 2,
+ newIndex: 1,
+ }
+ );
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ await PT.clearTransactionsHistory(false, true);
+ ensureUndoState([[bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]], 0);
+
+ // Test moving items between folders.
+ let folder_b_info = createTestFolderInfo("Folder B");
+ let folder_b_txn = PT.NewFolder(folder_b_info);
+ folder_b_info.guid = await folder_b_txn.transact();
+ ensureUndoState(
+ [[folder_b_txn], [bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]],
+ 0
+ );
+
+ moveTxn = PT.Move({
+ guid: bm_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ newIndex: bmsvc.DEFAULT_INDEX,
+ });
+ await moveTxn.transact();
+
+ ensureDo = () => {
+ ensureUndoState(
+ [[moveTxn], [folder_b_txn], [bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]],
+ 0
+ );
+ ensureItemsMoved({
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_a_info.guid,
+ newParentGuid: folder_b_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+ ensureUndo = () => {
+ ensureUndoState(
+ [[moveTxn], [folder_b_txn], [bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]],
+ 1
+ );
+ ensureItemsMoved({
+ guid: bm_a_info.guid,
+ oldParentGuid: folder_b_info.guid,
+ newParentGuid: folder_a_info.guid,
+ oldIndex: 0,
+ newIndex: 0,
+ });
+ observer.reset();
+ };
+
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+ await PT.redo();
+ ensureDo();
+ await PT.undo();
+ ensureUndo();
+
+ // Clean up
+ await PT.undo(); // folder_b_txn
+ await PT.undo(); // folder_a_txn + the bookmarks;
+ Assert.equal(observer.itemsRemoved.size, 5);
+ ensureUndoState(
+ [[moveTxn], [folder_b_txn], [bm_c_txn, bm_b_txn, bm_a_txn, folder_a_txn]],
+ 3
+ );
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_remove_folder() {
+ let folder_level_1_info = createTestFolderInfo("Folder Level 1");
+ let folder_level_2_info = { title: "Folder Level 2" };
+
+ let folder_level_1_txn = PT.NewFolder(folder_level_1_info);
+ let folder_level_2_txn;
+ let results = await PT.batch([
+ folder_level_1_txn,
+ previousResults => {
+ folder_level_2_info.parentGuid = previousResults[0];
+ return (folder_level_2_txn = PT.NewFolder(folder_level_2_info));
+ },
+ ]);
+ folder_level_1_info.guid = results[0];
+ folder_level_2_info.guid = results[1];
+
+ let original_folder_level_1_tree = await PlacesUtils.promiseBookmarksTree(
+ folder_level_1_info.guid
+ );
+ let original_folder_level_2_tree = Object.assign(
+ { parentGuid: original_folder_level_1_tree.guid },
+ original_folder_level_1_tree.children[0]
+ );
+
+ ensureUndoState([[folder_level_2_txn, folder_level_1_txn]]);
+ await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ observer.reset();
+
+ let remove_folder_2_txn = PT.Remove(folder_level_2_info);
+ await remove_folder_2_txn.transact();
+
+ ensureUndoState([
+ [remove_folder_2_txn],
+ [folder_level_2_txn, folder_level_1_txn],
+ ]);
+ await ensureItemsRemoved(folder_level_2_info);
+
+ // Undo Remove "Folder Level 2"
+ await PT.undo();
+ ensureUndoState(
+ [[remove_folder_2_txn], [folder_level_2_txn, folder_level_1_txn]],
+ 1
+ );
+ await ensureItemsAdded(folder_level_2_info);
+ await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ await PT.redo();
+ ensureUndoState([
+ [remove_folder_2_txn],
+ [folder_level_2_txn, folder_level_1_txn],
+ ]);
+ await ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo it again
+ await PT.undo();
+ ensureUndoState(
+ [[remove_folder_2_txn], [folder_level_2_txn, folder_level_1_txn]],
+ 1
+ );
+ await ensureItemsAdded(folder_level_2_info);
+ await ensureBookmarksTreeRestoredCorrectly(original_folder_level_2_tree);
+ observer.reset();
+
+ // Undo the creation of both folders
+ await PT.undo();
+ ensureUndoState(
+ [[remove_folder_2_txn], [folder_level_2_txn, folder_level_1_txn]],
+ 2
+ );
+ await ensureItemsRemoved(folder_level_2_info, folder_level_1_info);
+ observer.reset();
+
+ // Redo the creation of both folders
+ await PT.redo();
+ ensureUndoState(
+ [[remove_folder_2_txn], [folder_level_2_txn, folder_level_1_txn]],
+ 1
+ );
+ await ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(
+ original_folder_level_1_tree
+ );
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ await PT.redo();
+ ensureUndoState([
+ [remove_folder_2_txn],
+ [folder_level_2_txn, folder_level_1_txn],
+ ]);
+ await ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo everything one last time
+ await PT.undo();
+ ensureUndoState(
+ [[remove_folder_2_txn], [folder_level_2_txn, folder_level_1_txn]],
+ 1
+ );
+ await ensureItemsAdded(folder_level_2_info);
+ observer.reset();
+
+ await PT.undo();
+ ensureUndoState(
+ [[remove_folder_2_txn], [folder_level_2_txn, folder_level_1_txn]],
+ 2
+ );
+ await ensureItemsRemoved(folder_level_2_info, folder_level_2_info);
+ observer.reset();
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_add_and_remove_bookmarks_with_additional_info() {
+ const testURI = "http://add.remove.tag";
+ const TAG_1 = "TestTag1";
+ const TAG_2 = "TestTag2";
+
+ let folder_info = createTestFolderInfo();
+ folder_info.guid = await PT.NewFolder(folder_info).transact();
+ let ensureTags = ensureTagsForURI.bind(null, testURI);
+
+ // Check that the NewBookmark transaction preserves tags.
+ observer.reset();
+ let b1_info = { parentGuid: folder_info.guid, url: testURI, tags: [TAG_1] };
+ b1_info.guid = await PT.NewBookmark(b1_info).transact();
+ let b1_originalInfo = await PlacesUtils.promiseBookmarksTree(b1_info.guid);
+ ensureTags([TAG_1]);
+ await PT.undo();
+ ensureTags([]);
+
+ observer.reset();
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
+ ensureTags([TAG_1]);
+
+ // Check if the Remove transaction removes and restores tags of children
+ // correctly.
+ await PT.Remove(folder_info.guid).transact();
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
+ ensureTags([TAG_1]);
+
+ await PT.redo();
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(b1_originalInfo);
+ ensureTags([TAG_1]);
+
+ // * Check that no-op tagging (the uri is already tagged with TAG_1) is
+ // also a no-op on undo.
+ observer.reset();
+ let b2_info = {
+ parentGuid: folder_info.guid,
+ url: testURI,
+ tags: [TAG_1, TAG_2],
+ };
+ b2_info.guid = await PT.NewBookmark(b2_info).transact();
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureItemsRemoved(b2_info);
+ ensureTags([TAG_1]);
+
+ // Check if Remove correctly restores tags.
+ observer.reset();
+ await PT.redo();
+ ensureTags([TAG_1, TAG_2]);
+
+ // Test Remove for multiple items.
+ observer.reset();
+ await PT.Remove(b1_info.guid).transact();
+ await PT.Remove(b2_info.guid).transact();
+ await PT.Remove(folder_info.guid).transact();
+ await ensureItemsRemoved(b1_info, b2_info, folder_info);
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureItemsAdded(folder_info);
+ ensureTags([]);
+
+ observer.reset();
+ await PT.undo();
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ await PT.undo();
+ await ensureItemsAdded(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ // The redo calls below cleanup everything we did.
+ observer.reset();
+ await PT.redo();
+ await ensureItemsRemoved(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ await PT.redo();
+ // The tag containers are removed in async and take some time
+ let oldCountTag1 = 0;
+ let oldCountTag2 = 0;
+ let allTags = await bmsvc.fetchTags();
+ for (let i of allTags) {
+ if (i.name == TAG_1) {
+ oldCountTag1 = i.count;
+ }
+ if (i.name == TAG_2) {
+ oldCountTag2 = i.count;
+ }
+ }
+ await TestUtils.waitForCondition(async () => {
+ allTags = await bmsvc.fetchTags();
+ let newCountTag1 = 0;
+ let newCountTag2 = 0;
+ for (let i of allTags) {
+ if (i.name == TAG_1) {
+ newCountTag1 = i.count;
+ }
+ if (i.name == TAG_2) {
+ newCountTag2 = i.count;
+ }
+ }
+ return newCountTag1 == oldCountTag1 - 1 && newCountTag2 == oldCountTag2 - 1;
+ });
+ await ensureItemsRemoved(b2_info);
+
+ ensureTags([]);
+
+ observer.reset();
+ await PT.redo();
+ await ensureItemsRemoved(folder_info);
+ ensureTags([]);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_creating_and_removing_a_separator() {
+ let folder_info = createTestFolderInfo();
+ let separator_info = {};
+ let undoEntries = [];
+
+ observer.reset();
+ let folder_txn = PT.NewFolder(folder_info);
+ let separator_txn;
+ let results = await PT.batch([
+ folder_txn,
+ previousResults => {
+ separator_info.parentGuid = previousResults[0];
+ return (separator_txn = PT.NewSeparator(separator_info));
+ },
+ ]);
+ folder_info.guid = results[0];
+ separator_info.guid = results[1];
+
+ undoEntries.unshift([separator_txn, folder_txn]);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ await PT.redo();
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ let remove_sep_txn = PT.Remove(separator_info);
+ await remove_sep_txn.transact();
+ undoEntries.unshift([remove_sep_txn]);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsRemoved(separator_info);
+
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(separator_info);
+
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 2);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ await PT.redo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(folder_info, separator_info);
+
+ // Clear redo entries and check that |redo| does nothing
+ observer.reset();
+ await PT.clearTransactionsHistory(false, true);
+ undoEntries.shift();
+ ensureUndoState(undoEntries, 0);
+ await PT.redo();
+ ensureItemsAdded();
+ ensureItemsRemoved();
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_title() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test_create_item.com",
+ title: "Original Title",
+ };
+
+ function ensureTitleChange(aCurrentTitle) {
+ ensureItemsTitleChanged({
+ guid: bm_info.guid,
+ title: aCurrentTitle,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact();
+ ensureTitleChange("New Title");
+
+ observer.reset();
+ await PT.undo();
+ ensureTitleChange("Original Title");
+
+ observer.reset();
+ await PT.redo();
+ ensureTitleChange("New Title");
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureTitleChange("Original Title");
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_url() {
+ let oldURI = "http://old.test_editing_item_uri.com/";
+ let newURI = "http://new.test_editing_item_uri.com/";
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: oldURI,
+ tags: ["TestTag"],
+ };
+ function ensureURIAndTags(
+ aPreChangeURI,
+ aPostChangeURI,
+ aOLdURITagsPreserved
+ ) {
+ ensureItemsUrlChanged({
+ guid: bm_info.guid,
+ url: aPostChangeURI,
+ });
+ ensureTagsForURI(aPostChangeURI, bm_info.tags);
+ ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []);
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ ensureTagsForURI(oldURI, bm_info.tags);
+
+ // When there's a single bookmark for the same url, tags should be moved.
+ observer.reset();
+ await PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, false);
+
+ observer.reset();
+ await PT.redo();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, false);
+
+ // When there're multiple bookmarks for the same url, tags should be copied.
+ let bm2_info = Object.create(bm_info);
+ bm2_info.guid = await PT.NewBookmark(bm2_info).transact();
+ let bm3_info = Object.create(bm_info);
+ bm3_info.url = newURI;
+ bm3_info.guid = await PT.NewBookmark(bm3_info).transact();
+
+ observer.reset();
+ await PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+
+ observer.reset();
+ await PT.redo();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+ await PT.undo();
+ await PT.undo();
+ await PT.undo();
+ ensureItemsRemoved(bm3_info, bm2_info, bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_keyword() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test.edit.keyword/",
+ };
+ const KEYWORD = "test_keyword";
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "") {
+ ensureItemsKeywordChanged({
+ guid: bm_info.guid,
+ keyword: aCurrentKeyword,
+ });
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditKeyword({
+ guid: bm_info.guid,
+ keyword: KEYWORD,
+ postData: "postData",
+ }).transact();
+ ensureKeywordChange(KEYWORD);
+ let entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData");
+
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.redo();
+ ensureKeywordChange(KEYWORD);
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData");
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_keyword_null_postData() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test.edit.keyword/",
+ };
+ const KEYWORD = "test_keyword";
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "") {
+ ensureItemsKeywordChanged({
+ guid: bm_info.guid,
+ keyword: aCurrentKeyword,
+ });
+ }
+
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditKeyword({
+ guid: bm_info.guid,
+ keyword: KEYWORD,
+ postData: null,
+ }).transact();
+ ensureKeywordChange(KEYWORD);
+ let entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, null);
+
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.redo();
+ ensureKeywordChange(KEYWORD);
+ entry = await PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, null);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange();
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_edit_specific_keyword() {
+ let bm_info = {
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test.edit.keyword/",
+ };
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") {
+ ensureItemsKeywordChanged({
+ guid: bm_info.guid,
+ keyword: aCurrentKeyword,
+ });
+ }
+
+ await PlacesUtils.keywords.insert({
+ keyword: "kw1",
+ url: bm_info.url,
+ postData: "postData1",
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "kw2",
+ url: bm_info.url,
+ postData: "postData2",
+ });
+ bm_info.guid = await PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ await PT.EditKeyword({
+ guid: bm_info.guid,
+ keyword: "keyword",
+ oldKeyword: "kw2",
+ }).transact();
+ ensureKeywordChange("keyword", "kw2");
+ let entry = await PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData1");
+ entry = await PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData2");
+ entry = await PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange("kw2", "keyword");
+ entry = await PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData1");
+ entry = await PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData2");
+ entry = await PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ await PT.redo();
+ ensureKeywordChange("keyword", "kw2");
+ entry = await PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData1");
+ entry = await PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url);
+ Assert.equal(entry.postData, "postData2");
+ entry = await PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureKeywordChange("kw2");
+ await PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_tag_uri() {
+ // This also tests passing uri specs.
+ let bm_info_a = {
+ url: "http://bookmarked.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ let bm_info_b = {
+ url: "http://bookmarked2.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ };
+ let unbookmarked_uri = "http://un.bookmarked.uri";
+
+ let results = await PT.batch([
+ PT.NewBookmark(bm_info_a),
+ PT.NewBookmark(bm_info_b),
+ ]);
+ bm_info_a.guid = results[0];
+ bm_info_b.guid = results[1];
+
+ async function doTest(aInfo) {
+ let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
+ let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
+
+ let tagWillAlsoBookmark = new Set();
+ for (let url of urls) {
+ if (!(await bmsvc.fetch({ url }))) {
+ tagWillAlsoBookmark.add(url);
+ }
+ }
+
+ async function ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, tags);
+ Assert.ok(await bmsvc.fetch({ url }));
+ }
+ }
+ async function ensureTagsUnset() {
+ for (let url of urls) {
+ ensureTagsForURI(url, []);
+ if (tagWillAlsoBookmark.has(url)) {
+ Assert.ok(!(await bmsvc.fetch({ url })));
+ } else {
+ Assert.ok(await bmsvc.fetch({ url }));
+ }
+ }
+ }
+
+ await PT.Tag(aInfo).transact();
+ await ensureTagsSet();
+ await PT.undo();
+ await ensureTagsUnset();
+ await PT.redo();
+ await ensureTagsSet();
+ await PT.undo();
+ await ensureTagsUnset();
+ }
+
+ await doTest({ url: bm_info_a.url, tags: ["MyTag"] });
+ await doTest({ urls: [bm_info_a.url], tag: "MyTag" });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] });
+ await doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" });
+ // Duplicate URLs listed.
+ await doTest({
+ urls: [bm_info_a.url, bm_info_b.url, bm_info_a.url],
+ tag: "D",
+ });
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_untag_uri() {
+ let bm_info_a = {
+ url: "http://bookmarked.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ tags: ["A", "B"],
+ };
+ let bm_info_b = {
+ url: "http://bookmarked2.uri",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ tag: "B",
+ };
+
+ let results = await PT.batch([
+ PT.NewBookmark(bm_info_a),
+ PT.NewBookmark(bm_info_b),
+ ]);
+ ensureTagsForURI(bm_info_a.url, bm_info_a.tags);
+ ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]);
+ bm_info_a.guid = results[0];
+
+ bm_info_b.guid = results[1];
+
+ async function doTest(aInfo) {
+ let urls, tagsRemoved;
+ if (typeof aInfo == "string") {
+ urls = [aInfo];
+ tagsRemoved = [];
+ } else if (Array.isArray(aInfo)) {
+ urls = aInfo;
+ tagsRemoved = [];
+ } else {
+ urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
+ tagsRemoved = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
+ }
+
+ let preRemovalTags = new Map();
+ for (let url of urls) {
+ preRemovalTags.set(url, tagssvc.getTagsForURI(Services.io.newURI(url)));
+ }
+
+ function ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, preRemovalTags.get(url));
+ }
+ }
+ function ensureTagsUnset() {
+ for (let url of urls) {
+ let expectedTags = !tagsRemoved.length
+ ? []
+ : preRemovalTags.get(url).filter(tag => !tagsRemoved.includes(tag));
+ ensureTagsForURI(url, expectedTags);
+ }
+ }
+
+ await PT.Untag(aInfo).transact();
+ await ensureTagsUnset();
+ await PT.undo();
+ await ensureTagsSet();
+ await PT.redo();
+ await ensureTagsUnset();
+ await PT.undo();
+ await ensureTagsSet();
+ }
+
+ await doTest(bm_info_a);
+ await doTest(bm_info_b);
+ await doTest(bm_info_a.url);
+ await doTest(bm_info_b.url);
+ await doTest([bm_info_a.url, bm_info_b.url]);
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" });
+ await doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] });
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(async function test_sort_folder_by_name() {
+ let folder_info = createTestFolderInfo();
+
+ let url = "http://sort.by.name/";
+ let preSep = ["3", "2", "1"].map(i => ({ title: i, url }));
+ let sep = {};
+ let postSep = ["c", "b", "a"].map(l => ({ title: l, url }));
+ let originalOrder = [...preSep, sep, ...postSep];
+ let sortedOrder = [
+ ...preSep.slice(0).reverse(),
+ sep,
+ ...postSep.slice(0).reverse(),
+ ];
+
+ let folder_txn = await PT.NewFolder(folder_info);
+ let transactions = [folder_txn];
+ for (let info of originalOrder) {
+ transactions.push(previousResults => {
+ info.parentGuid = previousResults[0];
+ return info == sep ? PT.NewSeparator(info) : PT.NewBookmark(info);
+ });
+ }
+ let results = await PT.batch(transactions);
+ folder_info.guid = results[0];
+ for (let i = 0; i < originalOrder.length; ++i) {
+ originalOrder[i].guid = results[i + 1];
+ }
+
+ let folderContainer = PlacesUtils.getFolderContents(folder_info.guid).root;
+ function ensureOrder(aOrder) {
+ for (let i = 0; i < folderContainer.childCount; i++) {
+ Assert.equal(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid);
+ }
+ }
+
+ ensureOrder(originalOrder);
+ await PT.SortByName(folder_info.guid).transact();
+ ensureOrder(sortedOrder);
+ await PT.undo();
+ ensureOrder(originalOrder);
+ await PT.redo();
+ ensureOrder(sortedOrder);
+
+ // Cleanup
+ observer.reset();
+ await PT.undo();
+ ensureOrder(originalOrder);
+ await PT.undo();
+ ensureItemsRemoved(...originalOrder, folder_info);
+});
+
+add_task(async function test_copy() {
+ async function duplicate_and_test(aOriginalGuid) {
+ let txn = PT.Copy({
+ guid: aOriginalGuid,
+ newParentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ let duplicateGuid = await txn.transact();
+ let originalInfo = await PlacesUtils.promiseBookmarksTree(aOriginalGuid);
+ let duplicateInfo = await PlacesUtils.promiseBookmarksTree(duplicateGuid);
+ await ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false);
+
+ async function redo() {
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(originalInfo);
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(duplicateInfo);
+ }
+ async function undo() {
+ await PT.undo();
+ // also undo the original item addition.
+ await PT.undo();
+ await ensureNonExistent(aOriginalGuid, duplicateGuid);
+ }
+
+ await undo();
+ await redo();
+ await undo();
+ await redo();
+
+ // Cleanup. This also remove the original item.
+ await PT.undo();
+ observer.reset();
+ await PT.clearTransactionsHistory();
+ }
+
+ let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
+ Preferences.set("privacy.reduceTimerPrecision", false);
+
+ registerCleanupFunction(function () {
+ Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
+ });
+
+ // Test duplicating leafs (bookmark, separator, empty folder)
+ PT.NewBookmark({
+ url: "http://test.item.duplicate",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ annos: [{ name: "Anno", value: "AnnoValue" }],
+ });
+ let sepTxn = PT.NewSeparator({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 1,
+ });
+
+ let emptyFolderTxn = PT.NewFolder(createTestFolderInfo());
+ for (let txn of [sepTxn, emptyFolderTxn]) {
+ let guid = await txn.transact();
+ await duplicate_and_test(guid);
+ }
+
+ // Test duplicating a folder having some contents.
+ let results = await PT.batch([
+ PT.NewFolder(createTestFolderInfo()),
+ previousResults =>
+ PT.NewFolder({
+ parentGuid: previousResults[0],
+ title: "Nested Folder",
+ }),
+ // Insert a bookmark under the nested folder.
+ previousResults =>
+ PT.NewBookmark({
+ url: "http://nested.nested.bookmark",
+ parentGuid: previousResults[1],
+ }),
+ // Insert a separator below the nested folder
+ previousResults => PT.NewSeparator({ parentGuid: previousResults[0] }),
+ // And another bookmark.
+ previousResults =>
+ PT.NewBookmark({
+ url: "http://nested.bookmark",
+ parentGuid: previousResults[0],
+ }),
+ ]);
+ let filledFolderGuid = results[0];
+
+ await duplicate_and_test(filledFolderGuid);
+
+ // Cleanup
+ await PT.clearTransactionsHistory();
+});
+
+add_task(async function test_array_input_for_batch() {
+ let folderTxn = PT.NewFolder(createTestFolderInfo());
+ let folderGuid = await folderTxn.transact();
+
+ let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ await PT.batch([sep1_txn, sep2_txn]);
+ ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0);
+
+ let ensureChildCount = async function (count) {
+ let tree = await PlacesUtils.promiseBookmarksTree(folderGuid);
+ if (count == 0) {
+ Assert.ok(!("children" in tree));
+ } else {
+ Assert.equal(tree.children.length, count);
+ }
+ };
+
+ await ensureChildCount(2);
+ await PT.undo();
+ await ensureChildCount(0);
+ await PT.redo();
+ await ensureChildCount(2);
+ await PT.undo();
+ await ensureChildCount(0);
+
+ await PT.undo();
+ Assert.equal(await PlacesUtils.promiseBookmarksTree(folderGuid), null);
+
+ // Cleanup
+ await PT.clearTransactionsHistory();
+});
+
+add_task(async function test_invalid_uri_spec_throws() {
+ Assert.throws(
+ () =>
+ PT.NewBookmark({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "invalid uri spec",
+ title: "test bookmark",
+ }),
+ /invalid uri spec is not a valid URL/
+ );
+ Assert.throws(
+ () => PT.Tag({ tag: "TheTag", urls: ["invalid uri spec"] }),
+ /TypeError: URL constructor: invalid uri spec is not a valid URL/
+ );
+ Assert.throws(
+ () => PT.Tag({ tag: "TheTag", urls: ["about:blank", "invalid uri spec"] }),
+ /TypeError: URL constructor: invalid uri spec is not a valid URL/
+ );
+});
+
+add_task(async function test_remove_multiple() {
+ let results = await PT.batch([
+ PT.NewFolder({
+ title: "Test Folder",
+ parentGuid: menuGuid,
+ }),
+ previousResults =>
+ PT.NewFolder({
+ title: "Nested Test Folder",
+ parentGuid: previousResults[0],
+ }),
+ previousResults => PT.NewSeparator(previousResults[1]),
+ PT.NewBookmark({
+ url: "http://test.bookmark.removed",
+ parentGuid: menuGuid,
+ }),
+ ]);
+ let guids = [results[0], results[3]];
+
+ let originalInfos = [];
+ for (let guid of guids) {
+ originalInfos.push(await PlacesUtils.promiseBookmarksTree(guid));
+ }
+
+ await PT.Remove(guids).transact();
+ await ensureNonExistent(...guids);
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+ await PT.redo();
+ await ensureNonExistent(...guids);
+ await PT.undo();
+ await ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Undo the New* transactions batch.
+ await PT.undo();
+ await ensureNonExistent(...guids);
+
+ // Redo it.
+ await PT.redo();
+ await ensureBookmarksTreeRestoredCorrectlyExceptDates(...originalInfos);
+
+ // Redo remove.
+ await PT.redo();
+ await ensureNonExistent(...guids);
+
+ // Cleanup
+ await PT.clearTransactionsHistory();
+ observer.reset();
+});
+
+add_task(async function test_renameTag() {
+ let url = "http://test.edit.keyword/";
+ await PT.Tag({ url, tags: ["t1", "t2"] }).transact();
+ ensureTagsForURI(url, ["t1", "t2"]);
+
+ // Create bookmark queries that point to the modified tag.
+ let bm1 = await PlacesUtils.bookmarks.insert({
+ url: "place:tag=t2",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ let bm2 = await PlacesUtils.bookmarks.insert({
+ url: "place:tag=t2&sort=1",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ // This points to 2 tags, and as such won't be touched.
+ let bm3 = await PlacesUtils.bookmarks.insert({
+ url: "place:tag=t2&tag=t1",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ await PT.RenameTag({ oldTag: "t2", tag: "t3" }).transact();
+ ensureTagsForURI(url, ["t1", "t3"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t3",
+ "The fitst bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t3&sort=1",
+ "The second bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t3&tag=t1",
+ "The third bookmark has been updated"
+ );
+
+ await PT.undo();
+ ensureTagsForURI(url, ["t1", "t2"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t2",
+ "The fitst bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t2&sort=1",
+ "The second bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t2&tag=t1",
+ "The third bookmark has been restored"
+ );
+
+ await PT.redo();
+ ensureTagsForURI(url, ["t1", "t3"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t3",
+ "The fitst bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t3&sort=1",
+ "The second bookmark has been updated"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t3&tag=t1",
+ "The third bookmark has been updated"
+ );
+
+ await PT.undo();
+ ensureTagsForURI(url, ["t1", "t2"]);
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm1.guid)).url.href,
+ "place:tag=t2",
+ "The fitst bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm2.guid)).url.href,
+ "place:tag=t2&sort=1",
+ "The second bookmark has been restored"
+ );
+ Assert.equal(
+ (await PlacesUtils.bookmarks.fetch(bm3.guid)).url.href,
+ "place:tag=t2&tag=t1",
+ "The third bookmark has been restored"
+ );
+
+ await PT.undo();
+ ensureTagsForURI(url, []);
+
+ await PT.clearTransactionsHistory();
+ ensureUndoState();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_remove_invalid_url() {
+ let folderGuid = await PT.NewFolder({
+ title: "Test Folder",
+ parentGuid: menuGuid,
+ }).transact();
+
+ 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: folderGuid,
+ guid: folderedGuid,
+ }
+ );
+ });
+
+ let guids = [folderGuid, guid];
+ await PT.Remove(guids).transact();
+ await ensureNonExistent(...guids, folderedGuid);
+ // Shouldn't throw, should restore the folder but not the bookmarks.
+ await PT.undo();
+ await ensureNonExistent(guid, folderedGuid);
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch(folderGuid),
+ "The folder should have been re-created"
+ );
+ await PT.redo();
+ await ensureNonExistent(guids, folderedGuid);
+ // Cleanup
+ await PT.clearTransactionsHistory();
+ observer.reset();
+});
diff --git a/toolkit/components/places/tests/unit/test_autocomplete_match_fallbackTitle.js b/toolkit/components/places/tests/unit/test_autocomplete_match_fallbackTitle.js
new file mode 100644
index 0000000000..109ca1edce
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_autocomplete_match_fallbackTitle.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This is a test for the fallbackTitle argument of autocomplete_match.
+
+add_task(async function test_match() {
+ async function search(text) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ `
+ SELECT AUTOCOMPLETE_MATCH(:text, 'http://mozilla.org/', 'Main title',
+ NULL, NULL, 1, 1, NULL,
+ :matchBehavior, :searchBehavior,
+ 'Fallback title')
+ `,
+ {
+ text,
+ matchBehavior: Ci.mozIPlacesAutoComplete.MATCH_ANYWHERE,
+ searchBehavior: 643,
+ }
+ );
+ return !!rows[0].getResultByIndex(0);
+ }
+ Assert.ok(await search("mai"), "Match on main title");
+ Assert.ok(await search("fall"), "Match on fallback title");
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js b/toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js
new file mode 100644
index 0000000000..d7f0cf21c8
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmark-tags-changed_frequency.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test checks that changing a tag for a bookmark with multiple tags
+// notifies bookmark-tags-changed event only once, and not once per tag.
+
+add_task(async function run_test() {
+ let tags = ["a", "b", "c"];
+ let uri = Services.io.newURI("http://1.moz.org/");
+
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title: "Bookmark 1",
+ });
+ PlacesUtils.tagging.tagURI(uri, tags);
+
+ let promise = Promise.withResolvers();
+
+ let bookmarksObserver = {
+ _changedCount: 0,
+ handlePlacesEvents(events) {
+ for (let event of events) {
+ switch (event.type) {
+ case "bookmark-removed":
+ if (event.guid == bookmark.guid) {
+ PlacesUtils.observers.removeListener(
+ ["bookmark-removed"],
+ this.handlePlacesEvents
+ );
+ Assert.equal(this._changedCount, 2);
+ promise.resolve();
+ }
+ break;
+ case "bookmark-tags-changed":
+ Assert.equal(event.guid, bookmark.guid);
+ this._changedCount++;
+ break;
+ }
+ }
+ },
+ };
+ bookmarksObserver.handlePlacesEvents =
+ bookmarksObserver.handlePlacesEvents.bind(bookmarksObserver);
+ PlacesUtils.observers.addListener(
+ ["bookmark-removed", "bookmark-tags-changed"],
+ bookmarksObserver.handlePlacesEvents
+ );
+
+ PlacesUtils.tagging.tagURI(uri, ["d"]);
+ PlacesUtils.tagging.tagURI(uri, ["e"]);
+
+ await promise;
+
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html.js b/toolkit/components/places/tests/unit/test_bookmarks_html.js
new file mode 100644
index 0000000000..4b3f04b444
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js
@@ -0,0 +1,417 @@
+/* -*- 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/. */
+
+// An object representing the contents of bookmarks.preplaces.html.
+var test_bookmarks = {
+ menu: [
+ {
+ title: "Mozilla Firefox",
+ children: [
+ {
+ title: "Help and Tutorials",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/help/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ title: "Customize Firefox",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/customize/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ title: "Get Involved",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/community/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ title: "About Us",
+ url: "http://en-us.www.mozilla.com/en-US/about/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ ],
+ },
+ {
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR,
+ },
+ {
+ title: "test",
+ dateAdded: 1177541020000000,
+ lastModified: 1177541050000000,
+ children: [
+ {
+ title: "test post keyword",
+ dateAdded: 1177375336000000,
+ lastModified: 1177375423000000,
+ keyword: "test",
+ postData: "hidden1%3Dbar&text1%3D%25s",
+ charset: "ISO-8859-1",
+ url: "http://test/post",
+ },
+ ],
+ },
+ ],
+ toolbar: [
+ {
+ title: "Getting Started",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/central/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ title: "Latest Headlines",
+ url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+ },
+ // This will be ignored, because it has no url.
+ {
+ title: "Latest Headlines No Site",
+ feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+ ignore: true,
+ },
+ ],
+ unfiled: [{ title: "Example.tld", url: "http://example.tld/" }],
+};
+
+// Pre-Places bookmarks.html file pointer.
+var gBookmarksFileOld;
+// Places bookmarks.html file pointer.
+var gBookmarksFileNew;
+
+add_task(async function setup() {
+ // File pointer to legacy bookmarks file.
+ gBookmarksFileOld = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks.preplaces.html"
+ );
+
+ // File pointer to a new Places-exported bookmarks file.
+ gBookmarksFileNew = PathUtils.join(
+ PathUtils.profileDir,
+ "bookmarks.exported.html"
+ );
+ await IOUtils.remove(gBookmarksFileNew, { ignoreAbsent: true });
+
+ // This test must be the first one, since it setups the new bookmarks.html.
+ // Test importing a pre-Places canonical bookmarks file.
+ // 1. import bookmarks.preplaces.html
+ // 2. run the test-suite
+ // Note: we do not empty the db before this import to catch bugs like 380999
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileOld, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+
+ await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_count() {
+ // Ensure the bookmarks count is correct when importing in various cases
+ let count = await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, {
+ replace: true,
+ });
+ Assert.equal(
+ count,
+ 8,
+ "There should be 8 imported bookmarks when importing from an empty database"
+ );
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ count = -1;
+ count = await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, {
+ replace: true,
+ });
+ Assert.equal(
+ count,
+ 8,
+ "There should be 8 imported bookmarks when replacing existing bookmarks"
+ );
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ count = -1;
+ count = await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew);
+ Assert.equal(
+ count,
+ 8,
+ "There should be 8 imported bookmarks even when we are not replacing existing bookmarks"
+ );
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_new() {
+ // Test importing a Places bookmarks.html file.
+ // 1. import bookmarks.exported.html
+ // 2. run the test-suite
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ await testImportedBookmarks();
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_emptytitle_export() {
+ // Test exporting and importing with an empty-titled bookmark.
+ // 1. import bookmarks
+ // 2. create an empty-titled bookmark.
+ // 3. export to bookmarks.exported.html
+ // 4. empty bookmarks db
+ // 5. import bookmarks.exported.html
+ // 6. run the test-suite
+ // 7. remove the empty-titled bookmark
+ // 8. export to bookmarks.exported.html
+ // 9. empty bookmarks db and continue
+
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ const NOTITLE_URL = "http://notitle.mozilla.org/";
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: NOTITLE_URL,
+ });
+ test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL });
+
+ await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+
+ // Cleanup.
+ test_bookmarks.unfiled.pop();
+ // HTML imports don't restore GUIDs yet.
+ let reimportedBookmark = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+ Assert.equal(reimportedBookmark.url.href, bookmark.url.href);
+ await PlacesUtils.bookmarks.remove(reimportedBookmark);
+
+ await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_chromefavicon() {
+ // Test exporting and importing with a bookmark pointing to a chrome favicon.
+ // 1. import bookmarks
+ // 2. create a bookmark pointing to a chrome favicon.
+ // 3. export to bookmarks.exported.html
+ // 4. empty bookmarks db
+ // 5. import bookmarks.exported.html
+ // 6. run the test-suite
+ // 7. remove the bookmark pointing to a chrome favicon.
+ // 8. export to bookmarks.exported.html
+ // 9. empty bookmarks db and continue
+
+ const PAGE_URI = NetUtil.newURI("http://example.com/chromefavicon_page");
+ const CHROME_FAVICON_URI = NetUtil.newURI(
+ "chrome://global/skin/icons/delete.svg"
+ );
+ const CHROME_FAVICON_URI_2 = NetUtil.newURI(
+ "chrome://global/skin/icons/error.svg"
+ );
+
+ info("Importing from html");
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ info("Insert bookmark");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: PAGE_URI,
+ title: "Test",
+ });
+
+ info("Set favicon");
+ await new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ PAGE_URI,
+ CHROME_FAVICON_URI,
+ true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+
+ let data = await new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(
+ PAGE_URI,
+ (uri, dataLen, faviconData, mimeType) => resolve(faviconData)
+ );
+ });
+
+ let base64Icon =
+ "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+
+ test_bookmarks.unfiled.push({
+ title: "Test",
+ url: PAGE_URI.spec,
+ icon: base64Icon,
+ });
+
+ info("Export to html");
+ await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ info("Set favicon");
+ // Change the favicon to check it's really imported again later.
+ await new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ PAGE_URI,
+ CHROME_FAVICON_URI_2,
+ true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+
+ info("import from html");
+ await PlacesUtils.bookmarks.eraseEverything();
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ info("Test imported bookmarks");
+ await testImportedBookmarks();
+
+ // Cleanup.
+ test_bookmarks.unfiled.pop();
+ // HTML imports don't restore GUIDs yet.
+ let reimportedBookmark = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+ await PlacesUtils.bookmarks.remove(reimportedBookmark);
+
+ await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_ontop() {
+ // Test importing the exported bookmarks.html file *on top of* the existing
+ // bookmarks.
+ // 1. empty bookmarks db
+ // 2. import the exported bookmarks file
+ // 3. export to file
+ // 3. import the exported bookmarks file
+ // 4. run the test-suite
+
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ await BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+async function testImportedBookmarks() {
+ for (let group in test_bookmarks) {
+ info("[testImportedBookmarks()] Checking group '" + group + "'");
+
+ let root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks[`${group}Guid`]
+ ).root;
+
+ let items = test_bookmarks[group].filter(b => !b.ignore);
+ Assert.equal(root.childCount, items.length);
+
+ for (let key in items) {
+ await checkItem(items[key], root.getChild(key));
+ }
+
+ root.containerOpen = false;
+ }
+}
+
+function checkItem(aExpected, aNode) {
+ return (async function () {
+ let bookmark = await PlacesUtils.bookmarks.fetch(aNode.bookmarkGuid);
+
+ for (let prop in aExpected) {
+ switch (prop) {
+ case "type":
+ Assert.equal(aNode.type, aExpected.type);
+ break;
+ case "title":
+ Assert.equal(aNode.title, aExpected.title);
+ break;
+ case "dateAdded":
+ Assert.equal(
+ PlacesUtils.toPRTime(bookmark.dateAdded),
+ aExpected.dateAdded
+ );
+ break;
+ case "lastModified":
+ Assert.equal(
+ PlacesUtils.toPRTime(bookmark.lastModified),
+ aExpected.lastModified
+ );
+ break;
+ case "url":
+ Assert.equal(aNode.uri, aExpected.url);
+ break;
+ case "icon":
+ let { data } = await getFaviconDataForPage(aExpected.url);
+ let base64Icon =
+ "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+ Assert.ok(base64Icon == aExpected.icon);
+ break;
+ case "keyword": {
+ let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.keyword, aExpected.keyword);
+ break;
+ }
+ case "postData": {
+ let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.postData, aExpected.postData);
+ break;
+ }
+ case "charset":
+ let pageInfo = await PlacesUtils.history.fetch(aNode.uri, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ aExpected.charset
+ );
+ break;
+ case "feedUrl":
+ // No more supported.
+ break;
+ case "children":
+ let folder = aNode.QueryInterface(
+ Ci.nsINavHistoryContainerResultNode
+ );
+ Assert.equal(folder.hasChildren, !!aExpected.children.length);
+ folder.containerOpen = true;
+ Assert.equal(folder.childCount, aExpected.children.length);
+
+ for (let index = 0; index < aExpected.children.length; index++) {
+ await checkItem(aExpected.children[index], folder.getChild(index));
+ }
+
+ folder.containerOpen = false;
+ break;
+ default:
+ throw new Error("Unknown property");
+ }
+ }
+ })();
+}
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
new file mode 100644
index 0000000000..061c8c0c5f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
@@ -0,0 +1,126 @@
+/*
+ * This test ensures that importing/exporting to HTML does not stop
+ * if a malformed uri is found.
+ */
+
+const TEST_FAVICON_PAGE_URL =
+ "http://en-US.www.mozilla.com/en-US/firefox/central/";
+const TEST_FAVICON_DATA_SIZE = 580;
+
+add_task(async function test_corrupt_file() {
+ // Import bookmarks from the corrupt file.
+ let corruptHtml = PathUtils.join(do_get_cwd().path, "bookmarks.corrupt.html");
+ await BookmarkHTMLUtils.importFromFile(corruptHtml, { replace: true });
+
+ // Check that bookmarks that are not corrupt have been imported.
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await database_check();
+});
+
+add_task(async function test_corrupt_database() {
+ // Create corruption in the database, then export.
+ let corruptBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://test.mozilla.org",
+ title: "We love belugas",
+ });
+ await PlacesUtils.withConnectionWrapper("test", async function (db) {
+ await db.execute("UPDATE moz_bookmarks SET fk = NULL WHERE guid = :guid", {
+ guid: corruptBookmark.guid,
+ });
+ });
+
+ let bookmarksFile = PathUtils.join(
+ PathUtils.profileDir,
+ "bookmarks.exported.html"
+ );
+ await IOUtils.remove(bookmarksFile, { ignoreAbsent: true });
+ await BookmarkHTMLUtils.exportToFile(bookmarksFile);
+
+ // Import again and check for correctness.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await database_check();
+});
+
+/*
+ * Check for imported bookmarks correctness
+ *
+ * @return {Promise}
+ * @resolves When the checks are finished.
+ * @rejects Never.
+ */
+var database_check = async function () {
+ // BOOKMARKS MENU
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root;
+ Assert.equal(root.childCount, 2);
+
+ let folderNode = root.getChild(1);
+ Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
+ Assert.equal(folderNode.title, "test");
+
+ let bookmark = await PlacesUtils.bookmarks.fetch({
+ guid: folderNode.bookmarkGuid,
+ });
+ Assert.equal(PlacesUtils.toPRTime(bookmark.dateAdded), 1177541020000000);
+ Assert.equal(PlacesUtils.toPRTime(bookmark.lastModified), 1177541050000000);
+
+ // open test folder, and test the children
+ PlacesUtils.asQuery(folderNode);
+ Assert.equal(folderNode.hasChildren, true);
+ folderNode.containerOpen = true;
+ Assert.equal(folderNode.childCount, 1);
+
+ let bookmarkNode = folderNode.getChild(0);
+ Assert.equal("http://test/post", bookmarkNode.uri);
+ Assert.equal("test post keyword", bookmarkNode.title);
+
+ let entry = await PlacesUtils.keywords.fetch({ url: bookmarkNode.uri });
+ Assert.equal("test", entry.keyword);
+ Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData);
+
+ Assert.equal(bookmarkNode.dateAdded, 1177375336000000);
+ Assert.equal(bookmarkNode.lastModified, 1177375423000000);
+
+ let pageInfo = await PlacesUtils.history.fetch(bookmarkNode.uri, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ "ISO-8859-1",
+ "Should have the correct charset"
+ );
+
+ // clean up
+ folderNode.containerOpen = false;
+ root.containerOpen = false;
+
+ // BOOKMARKS TOOLBAR
+ root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.toolbarGuid).root;
+ Assert.equal(root.childCount, 3);
+
+ // cleanup
+ root.containerOpen = false;
+
+ // UNFILED BOOKMARKS
+ root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.unfiledGuid).root;
+ Assert.equal(root.childCount, 1);
+ root.containerOpen = false;
+
+ // favicons
+ await new Promise(resolve => {
+ PlacesUtils.favicons.getFaviconDataForPage(
+ uri(TEST_FAVICON_PAGE_URL),
+ (aURI, aDataLen, aData, aMimeType) => {
+ // aURI should never be null when aDataLen > 0.
+ Assert.notEqual(aURI, null);
+ // Favicon data is stored in the bookmarks file as a "data:" URI. For
+ // simplicity, instead of converting the data we receive to a "data:" URI
+ // and comparing it, we just check the data size.
+ Assert.equal(TEST_FAVICON_DATA_SIZE, aDataLen);
+ resolve();
+ }
+ );
+ });
+};
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_escape_entities.js b/toolkit/components/places/tests/unit/test_bookmarks_html_escape_entities.js
new file mode 100644
index 0000000000..5349d3948c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_escape_entities.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Checks that html entities are escaped in bookmarks.html files.
+
+add_task(async function () {
+ // Removes bookmarks.html if the file already exists.
+ let HTMLFile = PathUtils.join(PathUtils.profileDir, "bookmarks.html");
+ await IOUtils.remove(HTMLFile, { ignoreAbsent: true });
+
+ let unescaped = '<unescaped="test">';
+ // Adds bookmarks and tags to the database.
+ const url = 'http://www.google.it/"/';
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: unescaped,
+ });
+ await PlacesUtils.keywords.insert({
+ url,
+ keyword: unescaped,
+ postData: unescaped,
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), [unescaped]);
+ await PlacesUtils.history.update({
+ url,
+ annotations: new Map([[PlacesUtils.CHARSET_ANNO, unescaped]]),
+ });
+
+ // Exports the bookmarks as a HTML file.
+ await BookmarkHTMLUtils.exportToFile(HTMLFile);
+ await PlacesUtils.bookmarks.remove(bm);
+
+ // Check there are no unescaped entities in the html file.
+ let xml = await new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.onload = () => {
+ try {
+ resolve(xhr.responseXML);
+ } catch (e) {
+ reject(e);
+ }
+ };
+ xhr.onabort =
+ xhr.onerror =
+ xhr.ontimeout =
+ () => {
+ reject(new Error("xmlhttprequest failed"));
+ };
+ xhr.open("GET", PathUtils.toFileURI(HTMLFile));
+ xhr.responseType = "document";
+ xhr.overrideMimeType("text/html");
+ xhr.send();
+ });
+
+ let checksCount = 5;
+ for (
+ let current = xml;
+ current;
+ current =
+ current.firstChild ||
+ current.nextSibling ||
+ current.parentNode.nextSibling
+ ) {
+ switch (current.nodeType) {
+ case current.ELEMENT_NODE:
+ for (let { name, value } of current.attributes) {
+ info("Found attribute: " + name);
+ // Check tags, keyword, postData and charSet.
+ if (
+ ["tags", "last_charset", "shortcuturl", "post_data"].includes(name)
+ ) {
+ Assert.equal(
+ value,
+ unescaped,
+ `Attribute ${name} should be complete`
+ );
+ checksCount--;
+ }
+ }
+ break;
+ case current.TEXT_NODE:
+ // Check Title.
+ if (!current.data.startsWith("\n") && current.data.includes("test")) {
+ Assert.equal(
+ current.data.trim(),
+ unescaped,
+ "Text node should be complete"
+ );
+ checksCount--;
+ }
+ break;
+ }
+ }
+ Assert.equal(checksCount, 0, "All the checks ran");
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
new file mode 100644
index 0000000000..f01048e1a5
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
@@ -0,0 +1,64 @@
+var bookmarkData = [
+ {
+ uri: uri("http://www.toastytech.com"),
+ title: "Nathan's Toasty Technology Page",
+ tags: ["technology", "personal", "retro"],
+ },
+ {
+ uri: uri("http://www.reddit.com"),
+ title: "reddit: the front page of the internet",
+ tags: ["social media", "news", "humour"],
+ },
+ {
+ uri: uri("http://www.4chan.org"),
+ title: "4chan",
+ tags: ["discussion", "imageboard", "anime"],
+ },
+];
+
+/*
+ TEST SUMMARY
+ - Add bookmarks with tags
+ - Export tagged bookmarks as HTML file
+ - Delete bookmarks
+ - Import bookmarks from HTML file
+ - Check that all bookmarks are successfully imported with tags
+*/
+
+add_task(async function test_import_tags() {
+ // Removes bookmarks.html if the file already exists.
+ let HTMLFile = PathUtils.join(PathUtils.profileDir, "bookmarks.html");
+ await IOUtils.remove(HTMLFile, { ignoreAbsent: true });
+
+ // Adds bookmarks and tags to the database.
+ let bookmarkList = new Set();
+ for (let { uri, title, tags } of bookmarkData) {
+ bookmarkList.add(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title,
+ })
+ );
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
+
+ // Exports the bookmarks as a HTML file.
+ await BookmarkHTMLUtils.exportToFile(HTMLFile);
+
+ // Deletes bookmarks and tags from the database.
+ for (let bookmark of bookmarkList) {
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+ }
+
+ // Re-imports the bookmarks from the HTML file.
+ await BookmarkHTMLUtils.importFromFile(HTMLFile, { replace: true });
+
+ // Tests to ensure that the tags are still present for each bookmark URI.
+ for (let { uri, tags } of bookmarkData) {
+ info("Test tags for " + uri.spec + ": " + tags + "\n");
+ let foundTags = PlacesUtils.tagging.getTagsForURI(uri);
+ Assert.equal(foundTags.length, tags.length);
+ Assert.ok(tags.every(tag => foundTags.includes(tag)));
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_localized.js b/toolkit/components/places/tests/unit/test_bookmarks_html_localized.js
new file mode 100644
index 0000000000..4ef2393efb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_localized.js
@@ -0,0 +1,51 @@
+/* -*- 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/. */
+
+const { BookmarkHTMLUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/BookmarkHTMLUtils.sys.mjs"
+);
+
+add_task(async function setup_l10n() {
+ // A single localized string.
+ const mockSource = L10nFileSource.createMock(
+ "test",
+ "app",
+ ["en-US"],
+ "/localization/{locale}/",
+ [
+ {
+ path: "/localization/en-US/bookmarks_html_localized.ftl",
+ source: `
+bookmarks-html-localized-folder = Localized Folder
+bookmarks-html-localized-bookmark = Localized Bookmark
+`,
+ },
+ ]
+ );
+
+ L10nRegistry.getInstance().registerSources([mockSource]);
+});
+
+add_task(async function test_bookmarks_html_localized() {
+ let bookmarksFile = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks_html_localized.html"
+ );
+ await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true });
+
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root;
+ Assert.equal(root.childCount, 1);
+ let folder = root.getChild(0);
+ PlacesUtils.asContainer(folder).containerOpen = true;
+ // Folder title is localized.
+ Assert.equal(folder.title, "Localized Folder");
+ Assert.equal(folder.childCount, 1);
+ let bookmark = folder.getChild(0);
+ Assert.equal(bookmark.uri, "http://www.mozilla.com/firefox/help/");
+ // Bookmark title is localized.
+ Assert.equal(bookmark.title, "Localized Bookmark");
+ folder.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
new file mode 100644
index 0000000000..07131feafe
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
@@ -0,0 +1,31 @@
+/* -*- 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 for bug #801450
+
+// Get Services
+const { BookmarkHTMLUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/BookmarkHTMLUtils.sys.mjs"
+);
+
+add_task(async function test_bookmarks_html_singleframe() {
+ let bookmarksFile = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks_html_singleframe.html"
+ );
+ await BookmarkHTMLUtils.importFromFile(bookmarksFile, { replace: true });
+
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.menuGuid).root;
+ Assert.equal(root.childCount, 1);
+ let folder = root.getChild(0);
+ PlacesUtils.asContainer(folder).containerOpen = true;
+ Assert.equal(folder.title, "Subtitle");
+ Assert.equal(folder.childCount, 1);
+ let bookmark = folder.getChild(0);
+ Assert.equal(bookmark.uri, "http://www.mozilla.org/");
+ Assert.equal(bookmark.title, "Mozilla");
+ folder.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_json.js b/toolkit/components/places/tests/unit/test_bookmarks_json.js
new file mode 100644
index 0000000000..19aaac7f95
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -0,0 +1,368 @@
+/* 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 { BookmarkJSONUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/BookmarkJSONUtils.sys.mjs"
+);
+
+// An object representing the contents of bookmarks.json.
+var test_bookmarks = {
+ menu: [
+ {
+ guid: "OCyeUO5uu9FF",
+ title: "Mozilla Firefox",
+ children: [
+ {
+ guid: "OCyeUO5uu9FG",
+ title: "Help and Tutorials",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/help/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ guid: "OCyeUO5uu9FH",
+ title: "Customize Firefox",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/customize/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ guid: "OCyeUO5uu9FI",
+ title: "Get Involved",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/community/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ guid: "OCyeUO5uu9FJ",
+ title: "About Us",
+ url: "http://en-us.www.mozilla.com/en-US/about/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ guid: "QFM-QnE2ZpMz",
+ title: "Test null postData",
+ url: "http://example.com/search?q=%s&suggid=",
+ },
+ ],
+ },
+ {
+ guid: "OCyeUO5uu9FK",
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR,
+ },
+ {
+ guid: "OCyeUO5uu9FL",
+ title: "test",
+ dateAdded: 1177541020000000,
+ lastModified: 1177541050000000,
+ children: [
+ {
+ guid: "OCyeUO5uu9GX",
+ title: "test post keyword",
+ dateAdded: 1177375336000000,
+ lastModified: 1177375423000000,
+ keyword: "test",
+ postData: "hidden1%3Dbar&text1%3D%25s",
+ charset: "ISO-8859-1",
+ },
+ ],
+ },
+ ],
+ toolbar: [
+ {
+ guid: "OCyeUO5uu9FB",
+ title: "Getting Started",
+ url: "http://en-us.www.mozilla.com/en-US/firefox/central/",
+ icon: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABGdBTUEAAK/INwWK6QAAABl0RVh0U29mdHdhcmUAQWRvYmUgSW1hZ2VSZWFkeXHJZTwAAAHWSURBVHjaYvz//z8DJQAggJiQOe/fv2fv7Oz8rays/N+VkfG/iYnJfyD/1+rVq7ffu3dPFpsBAAHEAHIBCJ85c8bN2Nj4vwsDw/8zQLwKiO8CcRoQu0DxqlWrdsHUwzBAAIGJmTNnPgYa9j8UqhFElwPxf2MIDeIrKSn9FwSJoRkAEEAM0DD4DzMAyPi/G+QKY4hh5WAXGf8PDQ0FGwJ22d27CjADAAIIrLmjo+MXA9R2kAHvGBA2wwx6B8W7od6CeQcggKCmCEL8bgwxYCbUIGTDVkHDBia+CuotgACCueD3TDQN75D4xmAvCoK9ARMHBzAw0AECiBHkAlC0Mdy7x9ABNA3obAZXIAa6iKEcGlMVQHwWyjYuL2d4v2cPg8vZswx7gHyAAAK7AOif7SAbOqCmn4Ha3AHFsIDtgPq/vLz8P4MSkJ2W9h8ggBjevXvHDo4FQUQg/kdypqCg4H8lUIACnQ/SOBMYI8bAsAJFPcj1AAEEjwVQqLpAbXmH5BJjqI0gi9DTAAgDBBCcAVLkgmQ7yKCZxpCQxqUZhAECCJ4XgMl493ug21ZD+aDAXH0WLM4A9MZPXJkJIIAwTAR5pQMalaCABQUULttBGCCAGCnNzgABBgAMJ5THwGvJLAAAAABJRU5ErkJggg==",
+ },
+ {
+ guid: "OCyeUO5uu9FR",
+ title: "Latest Headlines",
+ // This used to be a livemark, but we don't import them anymore, instead
+ // it will be imported as an empty folder, because the json format stores
+ // it like that: an empty folder with a couple annotations. Since
+ // annotations will go away, there won't be a clean way to import it as a
+ // bookmark instead.
+ // Note: date gets truncated to milliseconds, whereas the value in bookmarks.json
+ // has full microseconds.
+ dateAdded: 1361551979451000,
+ lastModified: 1361551979457000,
+ },
+ ],
+ unfiled: [
+ { guid: "OCyeUO5uu9FW", title: "Example.tld", url: "http://example.tld/" },
+ {
+ guid: "Cfkety492Afk",
+ title: "test tagged bookmark",
+ dateAdded: 1507025843703000,
+ lastModified: 1507025844703000,
+ url: "http://example.tld/tagged",
+ tags: ["foo"],
+ },
+ {
+ guid: "lOZGoFR1eXbl",
+ title: "Bookmarks Toolbar Shortcut",
+ dateAdded: 1507025843703000,
+ lastModified: 1507025844703000,
+ url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`,
+ },
+ {
+ guid: "7yJWnBVhjRtP",
+ title: "Folder Shortcut",
+ dateAdded: 1507025843703000,
+ lastModified: 1507025844703000,
+ url: `place:parent=OCyeUO5uu9FF`,
+ },
+ {
+ guid: "vm5QXWuWc12l",
+ title: "Folder Shortcut 2",
+ dateAdded: 1507025843703000,
+ lastModified: 1507025844703000,
+ url: "place:invalidOldParentId=6123443&excludeItems=1",
+ },
+ {
+ guid: "Icg1XlIozA1D",
+ title: "Folder Shortcut 3",
+ dateAdded: 1507025843703000,
+ lastModified: 1507025844703000,
+ url: `place:parent=OCyeUO5uu9FF&parent=${PlacesUtils.bookmarks.menuGuid}`,
+ },
+ ],
+};
+
+// Exported bookmarks file pointer.
+var bookmarksExportedFile;
+
+add_task(async function test_import_bookmarks_disallowed_url() {
+ await Assert.rejects(
+ BookmarkJSONUtils.importFromURL("http://example.com/bookmarks.json"),
+ /importFromURL can only be used with/,
+ "Should reject importing from an http based url"
+ );
+ await Assert.rejects(
+ BookmarkJSONUtils.importFromURL("https://example.com/bookmarks.json"),
+ /importFromURL can only be used with/,
+ "Should reject importing from an https based url"
+ );
+});
+
+add_task(async function test_import_bookmarks_count() {
+ // Ensure the bookmarks count is correct when importing in various cases
+ await PlacesUtils.bookmarks.eraseEverything();
+ let bookmarksFile = PathUtils.join(do_get_cwd().path, "bookmarks.json");
+ bookmarksExportedFile = PathUtils.join(
+ PathUtils.profileDir,
+ "bookmarks.exported.json"
+ );
+
+ let count = await BookmarkJSONUtils.importFromFile(bookmarksFile, {
+ replace: true,
+ });
+ Assert.equal(
+ count,
+ 13,
+ "There should be 13 imported bookmarks when importing from an empty database"
+ );
+
+ await BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ count = -1;
+ count = await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, {
+ replace: true,
+ });
+ Assert.equal(
+ count,
+ 13,
+ "There should be 13 imported bookmarks when replacing existing bookmarks"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ count = -1;
+ let bookmarksUrl = PathUtils.toFileURI(bookmarksFile);
+ count = await BookmarkJSONUtils.importFromURL(bookmarksUrl);
+ Assert.equal(
+ count,
+ 13,
+ "There should be 13 imported bookmarks when importing from a URL"
+ );
+
+ // Clean up task
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_bookmarks() {
+ let bookmarksFile = PathUtils.join(do_get_cwd().path, "bookmarks.json");
+
+ await BookmarkJSONUtils.importFromFile(bookmarksFile, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+});
+
+add_task(async function test_export_bookmarks() {
+ bookmarksExportedFile = PathUtils.join(
+ PathUtils.profileDir,
+ "bookmarks.exported.json"
+ );
+ await BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(async function test_import_exported_bookmarks() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, {
+ replace: true,
+ });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+});
+
+add_task(async function test_import_ontop() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, {
+ replace: true,
+ });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, {
+ replace: true,
+ });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+});
+
+add_task(async function test_import_iconuri() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ let bookmarksFile = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks_iconuri.json"
+ );
+
+ await BookmarkJSONUtils.importFromFile(bookmarksFile, {
+ replace: true,
+ });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+});
+
+add_task(async function test_export_bookmarks_with_iconuri() {
+ bookmarksExportedFile = PathUtils.join(
+ PathUtils.profileDir,
+ "bookmarks.exported.json"
+ );
+ await BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(async function test_import_exported_bookmarks_with_iconuri() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await BookmarkJSONUtils.importFromFile(bookmarksExportedFile, {
+ replace: true,
+ });
+ await PlacesTestUtils.promiseAsyncUpdates();
+ await testImportedBookmarks();
+});
+
+add_task(async function test_clean() {
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+async function testImportedBookmarks() {
+ for (let group in test_bookmarks) {
+ info("[testImportedBookmarks()] Checking group '" + group + "'");
+
+ let root = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks[`${group}Guid`]
+ ).root;
+
+ let items = test_bookmarks[group];
+ Assert.equal(root.childCount, items.length);
+
+ for (let key in items) {
+ await checkItem(items[key], root.getChild(key));
+ }
+
+ root.containerOpen = false;
+ }
+}
+
+async function checkItem(aExpected, aNode) {
+ let bookmark = await PlacesUtils.bookmarks.fetch(aNode.bookmarkGuid);
+
+ for (let prop in aExpected) {
+ switch (prop) {
+ case "type":
+ Assert.equal(aNode.type, aExpected.type);
+ break;
+ case "title":
+ Assert.equal(aNode.title, aExpected.title);
+ break;
+ case "dateAdded":
+ Assert.equal(
+ PlacesUtils.toPRTime(bookmark.dateAdded),
+ aExpected.dateAdded
+ );
+ break;
+ case "lastModified":
+ Assert.equal(
+ PlacesUtils.toPRTime(bookmark.lastModified),
+ aExpected.lastModified
+ );
+ break;
+ case "url":
+ Assert.equal(aNode.uri, aExpected.url);
+ break;
+ case "icon":
+ let { data } = await getFaviconDataForPage(aExpected.url);
+ let base64Icon =
+ "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+ Assert.equal(base64Icon, aExpected.icon);
+ break;
+ case "keyword": {
+ let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.keyword, aExpected.keyword);
+ break;
+ }
+ case "guid":
+ Assert.equal(bookmark.guid, aExpected.guid);
+ break;
+ case "postData": {
+ let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.postData, aExpected.postData);
+ break;
+ }
+ case "charset":
+ let pageInfo = await PlacesUtils.history.fetch(aNode.uri, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ aExpected.charset
+ );
+ break;
+ case "children":
+ let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ Assert.equal(folder.hasChildren, !!aExpected.children.length);
+ folder.containerOpen = true;
+ Assert.equal(folder.childCount, aExpected.children.length);
+
+ for (let index = 0; index < aExpected.children.length; index++) {
+ await checkItem(aExpected.children[index], folder.getChild(index));
+ }
+
+ folder.containerOpen = false;
+ break;
+ case "tags":
+ let uri = Services.io.newURI(aNode.uri);
+ Assert.deepEqual(
+ PlacesUtils.tagging.getTagsForURI(uri),
+ aExpected.tags,
+ "should have the expected tags"
+ );
+ break;
+ default:
+ throw new Error("Unknown property");
+ }
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js b/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.js
new file mode 100644
index 0000000000..b31f1da5bb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json_corrupt.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/. */
+
+/**
+ * Tests for importing a corrupt json file.
+ *
+ * The corrupt json file attempts to import into:
+ * - the menu folder:
+ * - A bookmark with an invalid type.
+ * - A valid bookmark.
+ * - A bookmark with an invalid url.
+ * - the toolbar folder:
+ * - A bookmark with an invalid url.
+ *
+ * The menu case ensure that we strip out invalid bookmarks, but retain valid
+ * ones.
+ * The toolbar case ensures that if no valid bookmarks remain, then we do not
+ * throw an error.
+ */
+
+const { BookmarkJSONUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/BookmarkJSONUtils.sys.mjs"
+);
+
+add_task(async function test_import_bookmarks() {
+ let bookmarksFile = PathUtils.join(
+ do_get_cwd().path,
+ "bookmarks_corrupt.json"
+ );
+
+ await BookmarkJSONUtils.importFromFile(bookmarksFile, { replace: true });
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ let bookmarks = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ Assert.equal(
+ bookmarks.children.length,
+ 1,
+ "should only be one bookmark in the menu"
+ );
+ let bookmark = bookmarks.children[0];
+ Assert.equal(bookmark.guid, "OCyeUO5uu9FH", "should have correct guid");
+ Assert.equal(
+ bookmark.title,
+ "Customize Firefox",
+ "should have correct title"
+ );
+ Assert.equal(
+ bookmark.uri,
+ "http://en-us.www.mozilla.com/en-US/firefox/customize/",
+ "should have correct uri"
+ );
+
+ bookmarks = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+
+ Assert.ok(
+ !bookmarks.children,
+ "should not have any bookmarks in the toolbar"
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
new file mode 100644
index 0000000000..892b2d1d04
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
@@ -0,0 +1,319 @@
+/* -*- 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/. */
+
+const { BookmarkHTMLUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/BookmarkHTMLUtils.sys.mjs"
+);
+
+/**
+ * Tests the bookmarks-restore-* nsIObserver notifications after restoring
+ * bookmarks from JSON and HTML. See bug 470314.
+ */
+
+// The topics and data passed to nsIObserver.observe() on bookmarks restore
+const NSIOBSERVER_TOPIC_BEGIN = "bookmarks-restore-begin";
+const NSIOBSERVER_TOPIC_SUCCESS = "bookmarks-restore-success";
+const NSIOBSERVER_TOPIC_FAILED = "bookmarks-restore-failed";
+const NSIOBSERVER_DATA_JSON = "json";
+const NSIOBSERVER_DATA_HTML = "html";
+const NSIOBSERVER_DATA_HTML_INIT = "html-initial";
+
+// Bookmarks are added for these URIs
+var uris = [
+ "http://example.com/1",
+ "http://example.com/2",
+ "http://example.com/3",
+ "http://example.com/4",
+ "http://example.com/5",
+];
+
+/**
+ * Adds some bookmarks for the URIs in |uris|.
+ */
+async function addBookmarks() {
+ for (let url of uris) {
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+ Assert.ok(await PlacesUtils.bookmarks.fetch({ url }), "Url is bookmarked");
+ }
+}
+
+/**
+ * Creates an file in the profile directory.
+ *
+ * @param aBasename
+ * e.g., "foo.txt" in the path /some/long/path/foo.txt
+ * @return {Promise}
+ * @resolves to an OS.File path
+ */
+async function promiseFile(aBasename) {
+ let path = PathUtils.join(PathUtils.profileDir, aBasename);
+ info("opening " + path);
+
+ await IOUtils.writeUTF8(path, "");
+ return path;
+}
+
+/**
+ * Register observers via promiseTopicObserved helper.
+ *
+ * @param {boolean} expectSuccess pass true when expect a success notification
+ * @return {Promise[]}
+ */
+function registerObservers(expectSuccess) {
+ let promiseBegin = promiseTopicObserved(NSIOBSERVER_TOPIC_BEGIN);
+ let promiseResult;
+ if (expectSuccess) {
+ promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_SUCCESS);
+ } else {
+ promiseResult = promiseTopicObserved(NSIOBSERVER_TOPIC_FAILED);
+ }
+
+ return [promiseBegin, promiseResult];
+}
+
+/**
+ * Check notification results.
+ *
+ * @param {Promise[]} expectPromises array contain promiseBegin and promiseResult
+ * @param {object} expectedData contain data and folderId
+ */
+async function checkObservers(expectPromises, expectedData) {
+ let [promiseBegin, promiseResult] = expectPromises;
+
+ let beginData = (await promiseBegin)[1];
+ Assert.equal(
+ beginData,
+ expectedData.data,
+ "Data for current test should be what is expected"
+ );
+
+ let [resultSubject, resultData] = await promiseResult;
+ Assert.equal(
+ resultData,
+ expectedData.data,
+ "Data for current test should be what is expected"
+ );
+
+ // Make sure folder ID is what is expected. For importing HTML into a
+ // folder, this will be an integer, otherwise null.
+ if (resultSubject) {
+ Assert.equal(
+ resultSubject.QueryInterface(Ci.nsISupportsPRInt64).data,
+ expectedData.folderId
+ );
+ } else {
+ Assert.equal(expectedData.folderId, null);
+ }
+}
+
+/**
+ * Run after every test cases.
+ */
+async function teardown(file, begin, success, fail) {
+ // On restore failed, file may not exist, so wrap in try-catch.
+ await IOUtils.remove(file, { ignoreAbsent: true });
+
+ // clean up bookmarks
+ await PlacesUtils.bookmarks.eraseEverything();
+}
+
+add_task(async function test_json_restore_normal() {
+ // data: the data passed to nsIObserver.observe() corresponding to the test
+ // folderId: for HTML restore into a folder, the folder ID to restore into;
+ // otherwise, set it to null
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(true);
+
+ info("JSON restore: normal restore should succeed");
+ let file = await promiseFile("bookmarks-test_restoreNotification.json");
+ await addBookmarks();
+
+ await BookmarkJSONUtils.exportToFile(file);
+ await PlacesUtils.bookmarks.eraseEverything();
+ try {
+ await BookmarkJSONUtils.importFromFile(file, { replace: true });
+ } catch (e) {
+ do_throw(" Restore should not have failed " + e);
+ }
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file);
+});
+
+add_task(async function test_json_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(false);
+
+ info("JSON restore: empty file should fail");
+ let file = await promiseFile("bookmarks-test_restoreNotification.json");
+ await Assert.rejects(
+ BookmarkJSONUtils.importFromFile(file, { replace: true }),
+ /SyntaxError/,
+ "Restore should reject for an empty file."
+ );
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file);
+});
+
+add_task(async function test_json_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(false);
+
+ info("JSON restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("this file doesn't exist because nobody created it 1");
+ await Assert.rejects(
+ BookmarkJSONUtils.importFromFile(file.path, { replace: true }),
+ /Cannot restore from nonexisting json file/,
+ "Restore should reject for a non-existent file."
+ );
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file.path);
+});
+
+add_task(async function test_html_restore_normal() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(true);
+
+ info("HTML restore: normal restore should succeed");
+ let file = await promiseFile("bookmarks-test_restoreNotification.html");
+ await addBookmarks();
+ await BookmarkHTMLUtils.exportToFile(file);
+ await PlacesUtils.bookmarks.eraseEverything();
+ try {
+ BookmarkHTMLUtils.importFromFile(file).catch(
+ do_report_unexpected_exception
+ );
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file);
+});
+
+add_task(async function test_html_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(true);
+
+ info("HTML restore: empty file should succeed");
+ let file = await promiseFile("bookmarks-test_restoreNotification.init.html");
+ try {
+ BookmarkHTMLUtils.importFromFile(file).catch(
+ do_report_unexpected_exception
+ );
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file);
+});
+
+add_task(async function test_html_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(false);
+
+ info("HTML restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("this file doesn't exist because nobody created it 2");
+ await Assert.rejects(
+ BookmarkHTMLUtils.importFromFile(file.path),
+ /Cannot import from nonexisting html file/,
+ "Restore should reject for a non-existent file."
+ );
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file.path);
+});
+
+add_task(async function test_html_init_restore_normal() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(true);
+
+ info("HTML initial restore: normal restore should succeed");
+ let file = await promiseFile("bookmarks-test_restoreNotification.init.html");
+ await addBookmarks();
+ await BookmarkHTMLUtils.exportToFile(file);
+ await PlacesUtils.bookmarks.eraseEverything();
+ try {
+ BookmarkHTMLUtils.importFromFile(file, { replace: true }).catch(
+ do_report_unexpected_exception
+ );
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file);
+});
+
+add_task(async function test_html_init_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(true);
+
+ info("HTML initial restore: empty file should succeed");
+ let file = await promiseFile("bookmarks-test_restoreNotification.init.html");
+ try {
+ BookmarkHTMLUtils.importFromFile(file, { replace: true }).catch(
+ do_report_unexpected_exception
+ );
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file);
+});
+
+add_task(async function test_html_init_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null,
+ };
+ let expectPromises = registerObservers(false);
+
+ info("HTML initial restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append("this file doesn't exist because nobody created it 3");
+ await Assert.rejects(
+ BookmarkHTMLUtils.importFromFile(file.path, { replace: true }),
+ /Cannot import from nonexisting html file/,
+ "Restore should reject for a non-existent file."
+ );
+
+ await checkObservers(expectPromises, expectedData);
+ await teardown(file.path);
+});
diff --git a/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js
new file mode 100644
index 0000000000..1e1f7bc9c7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_brokenFolderShortcut() {
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ url: "http://1.moz.org/",
+ title: "Bookmark 1",
+ },
+ {
+ url: "place:parent=1234",
+ title: "Shortcut 1",
+ },
+ {
+ url: "place:parent=-1",
+ title: "Shortcut 2",
+ },
+ {
+ url: "http://2.moz.org/",
+ title: "Bookmark 2",
+ },
+ ],
+ });
+
+ // Add also a simple visit.
+ await PlacesTestUtils.addVisits(uri("http://3.moz.org/"));
+
+ // Query containing a broken folder shortcuts among results.
+ let query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.unfiledGuid]);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ Assert.equal(root.childCount, 4);
+
+ let shortcut = root.getChild(1);
+ Assert.equal(shortcut.uri, "place:parent=1234");
+ PlacesUtils.asContainer(shortcut);
+ shortcut.containerOpen = true;
+ Assert.equal(shortcut.childCount, 0);
+ shortcut.containerOpen = false;
+ // Remove the broken shortcut while the containing result is open.
+ await PlacesUtils.bookmarks.remove(bookmarks[1]);
+ Assert.equal(root.childCount, 3);
+
+ shortcut = root.getChild(1);
+ Assert.equal(shortcut.uri, "place:parent=-1");
+ PlacesUtils.asContainer(shortcut);
+ shortcut.containerOpen = true;
+ Assert.equal(shortcut.childCount, 0);
+ shortcut.containerOpen = false;
+ // Remove the broken shortcut while the containing result is open.
+ await PlacesUtils.bookmarks.remove(bookmarks[2]);
+ Assert.equal(root.childCount, 2);
+
+ root.containerOpen = false;
+
+ // Broken folder shortcut as root node.
+ query = PlacesUtils.history.getNewQuery();
+ query.setParents([1234]);
+ options = PlacesUtils.history.getNewQueryOptions();
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 0);
+ root.containerOpen = false;
+
+ // Broken folder shortcut as root node with folder=-1.
+ query = PlacesUtils.history.getNewQuery();
+ query.setParents([-1]);
+ options = PlacesUtils.history.getNewQueryOptions();
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 0);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_browserhistory.js b/toolkit/components/places/tests/unit/test_browserhistory.js
new file mode 100644
index 0000000000..f737262ae7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_browserhistory.js
@@ -0,0 +1,122 @@
+/* -*- 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/. */
+
+const TEST_URI = "http://mozilla.com/";
+const TEST_SUBDOMAIN_URI = "http://foobar.mozilla.com/";
+
+async function checkEmptyHistory() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached("SELECT count(*) FROM moz_historyvisits");
+ return !rows[0].getResultByIndex(0);
+}
+
+add_task(async function test_addPage() {
+ await PlacesTestUtils.addVisits(TEST_URI);
+ Assert.ok(!(await checkEmptyHistory()), "History has entries");
+});
+
+add_task(async function test_removePage() {
+ await PlacesUtils.history.remove(TEST_URI);
+ Assert.ok(await checkEmptyHistory(), "History is empty");
+});
+
+add_task(async function test_removePages() {
+ let pages = [];
+ for (let i = 0; i < 8; i++) {
+ pages.push(TEST_URI + i);
+ }
+
+ await PlacesTestUtils.addVisits(pages.map(uri => ({ uri })));
+ // Bookmarked item should not be removed from moz_places.
+ const ANNO_INDEX = 1;
+ const ANNO_NAME = "testAnno";
+ const ANNO_VALUE = "foo";
+ const BOOKMARK_INDEX = 2;
+ await PlacesUtils.history.update({
+ url: pages[ANNO_INDEX],
+ annotations: new Map([[ANNO_NAME, ANNO_VALUE]]),
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: pages[BOOKMARK_INDEX],
+ title: "test bookmark",
+ });
+ await PlacesUtils.history.update({
+ url: pages[BOOKMARK_INDEX],
+ annotations: new Map([[ANNO_NAME, ANNO_VALUE]]),
+ });
+
+ await PlacesUtils.history.remove(pages);
+ Assert.ok(await checkEmptyHistory(), "History is empty");
+
+ // Check that the bookmark and its annotation still exist.
+ let folder = await PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+ Assert.equal(folder.root.childCount, 1);
+ let pageInfo = await PlacesUtils.history.fetch(pages[BOOKMARK_INDEX], {
+ includeAnnotations: true,
+ });
+ Assert.equal(pageInfo.annotations.get(ANNO_NAME), ANNO_VALUE);
+
+ // Check the annotation on the non-bookmarked page does not exist anymore.
+ await assertNoOrphanPageAnnotations();
+
+ // Cleanup.
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_removePagesByTimeframe() {
+ let visits = [];
+ let startDate = (Date.now() - 10000) * 1000;
+ for (let i = 0; i < 10; i++) {
+ visits.push({
+ uri: TEST_URI + i,
+ visitDate: startDate + i * 1000,
+ });
+ }
+
+ await PlacesTestUtils.addVisits(visits);
+
+ // Delete all pages except the first and the last.
+ await PlacesUtils.history.removeByFilter({
+ beginDate: PlacesUtils.toDate(startDate + 1000),
+ endDate: PlacesUtils.toDate(startDate + 8000),
+ });
+
+ // Check that we have removed the correct pages.
+ for (let i = 0; i < 10; i++) {
+ Assert.equal(page_in_database(TEST_URI + i) == 0, i > 0 && i < 9);
+ }
+
+ // Clear remaining items and check that all pages have been removed.
+ await PlacesUtils.history.removeByFilter({
+ beginDate: PlacesUtils.toDate(startDate),
+ endDate: PlacesUtils.toDate(startDate + 9000),
+ });
+ Assert.ok(await checkEmptyHistory(), "History is empty");
+});
+
+add_task(async function test_removePagesFromHost() {
+ await PlacesTestUtils.addVisits(TEST_URI);
+ await PlacesUtils.history.removeByFilter({ host: ".mozilla.com" });
+ Assert.ok(await checkEmptyHistory(), "History is empty");
+});
+
+add_task(async function test_removePagesFromHost_keepSubdomains() {
+ await PlacesTestUtils.addVisits([
+ { uri: TEST_URI },
+ { uri: TEST_SUBDOMAIN_URI },
+ ]);
+ await PlacesUtils.history.removeByFilter({ host: "mozilla.com" });
+ Assert.ok(!(await checkEmptyHistory()), "History has entries");
+});
+
+add_task(async function test_history_clear() {
+ await PlacesUtils.history.clear();
+ Assert.ok(await checkEmptyHistory(), "History is empty");
+});
diff --git a/toolkit/components/places/tests/unit/test_childlessTags.js b/toolkit/components/places/tests/unit/test_childlessTags.js
new file mode 100644
index 0000000000..9f489a266b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_childlessTags.js
@@ -0,0 +1,140 @@
+/* -*- 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/. */
+
+/**
+ * Ensures that removal of a bookmark untags the bookmark if it's no longer
+ * contained in any regular, non-tag folders. See bug 444849.
+ */
+
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+);
+
+var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService(
+ Ci.nsITaggingService
+);
+
+const BOOKMARK_URI = uri("http://example.com/");
+
+add_task(async function test_removing_tagged_bookmark_removes_tag() {
+ print(" Make a bookmark.");
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: BOOKMARK_URI,
+ title: "test bookmark",
+ });
+
+ print(" Tag it up.");
+ let tags = ["foo", "bar"];
+ tagssvc.tagURI(BOOKMARK_URI, tags);
+ ensureTagsExist(tags);
+ let root = getTagRoot();
+ root.containerOpen = true;
+ let oldCount = root.childCount;
+ root.containerOpen = false;
+
+ print(" Remove the bookmark. The tags should no longer exist.");
+ let wait = TestUtils.waitForCondition(() => {
+ root = getTagRoot();
+ root.containerOpen = true;
+ let val = root.childCount == oldCount - 2;
+ root.containerOpen = false;
+ return val;
+ });
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+ await wait;
+ ensureTagsExist([]);
+});
+
+add_task(
+ async function test_removing_folder_containing_tagged_bookmark_removes_tag() {
+ print(" Make a folder.");
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+
+ print(" Stick a bookmark in the folder.");
+ var bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ url: BOOKMARK_URI,
+ title: "test bookmark",
+ });
+
+ print(" Tag the bookmark.");
+ var tags = ["foo", "bar"];
+ tagssvc.tagURI(BOOKMARK_URI, tags);
+ ensureTagsExist(tags);
+
+ // The tag containers are removed in async and take some time
+ let oldCountFoo = await tagCount("foo");
+ let oldCountBar = await tagCount("bar");
+
+ print(" Remove the folder. The tags should no longer exist.");
+
+ let wait = TestUtils.waitForCondition(async () => {
+ let newCountFoo = await tagCount("foo");
+ let newCountBar = await tagCount("bar");
+ return newCountFoo == oldCountFoo - 1 && newCountBar == oldCountBar - 1;
+ });
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+ await wait;
+ ensureTagsExist([]);
+ }
+);
+
+async function tagCount(aTag) {
+ let allTags = await PlacesUtils.bookmarks.fetchTags();
+ for (let i of allTags) {
+ if (i.name == aTag) {
+ return i.count;
+ }
+ }
+ return 0;
+}
+
+function getTagRoot() {
+ var query = histsvc.getNewQuery();
+ var opts = histsvc.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_TAGS_ROOT;
+ var resultRoot = histsvc.executeQuery(query, opts).root;
+ return resultRoot;
+}
+/**
+ * Runs a tag query and ensures that the tags returned are those and only those
+ * in aTags. aTags may be empty, in which case this function ensures that no
+ * tags exist.
+ *
+ * @param aTags
+ * An array of tags (strings)
+ */
+function ensureTagsExist(aTags) {
+ var query = histsvc.getNewQuery();
+ var opts = histsvc.getNewQueryOptions();
+ opts.resultType = opts.RESULTS_AS_TAGS_ROOT;
+ var resultRoot = histsvc.executeQuery(query, opts).root;
+
+ // Dupe aTags.
+ var tags = aTags.slice(0);
+
+ resultRoot.containerOpen = true;
+
+ // Ensure that the number of tags returned from the query is the same as the
+ // number in |tags|.
+ Assert.equal(resultRoot.childCount, tags.length);
+
+ // For each tag result from the query, ensure that it's contained in |tags|.
+ // Remove the tag from |tags| so that we ensure the sets are equal.
+ for (let i = 0; i < resultRoot.childCount; i++) {
+ var tag = resultRoot.getChild(i).title;
+ var indexOfTag = tags.indexOf(tag);
+ Assert.ok(indexOfTag >= 0);
+ tags.splice(indexOfTag, 1);
+ }
+
+ resultRoot.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_frecency_decay.js b/toolkit/components/places/tests/unit/test_frecency_decay.js
new file mode 100644
index 0000000000..8fbb08aecc
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_decay.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+const PREF_FREC_DECAY_RATE_DEF = 0.975;
+
+/**
+ * Promises that the pages-rank-changed event has been seen.
+ *
+ * @returns {Promise} A promise which is resolved when the notification is seen.
+ */
+function promiseRankingChanged() {
+ return PlacesTestUtils.waitForNotification("pages-rank-changed");
+}
+
+add_task(async function setup() {
+ Services.prefs.setCharPref(
+ "places.frecency.decayRate",
+ PREF_FREC_DECAY_RATE_DEF
+ );
+});
+
+add_task(async function test_isFrecencyDecaying() {
+ let db = await PlacesUtils.promiseDBConnection();
+ async function queryFrecencyDecaying() {
+ return (
+ await db.executeCached(`SELECT is_frecency_decaying()`)
+ )[0].getResultByIndex(0);
+ }
+ PlacesUtils.history.isFrecencyDecaying = true;
+ Assert.equal(PlacesUtils.history.isFrecencyDecaying, true);
+ Assert.equal(await queryFrecencyDecaying(), true);
+ PlacesUtils.history.isFrecencyDecaying = false;
+ Assert.equal(PlacesUtils.history.isFrecencyDecaying, false);
+ Assert.equal(await queryFrecencyDecaying(), false);
+});
+
+add_task(async function test_frecency_decay() {
+ let unvisitedBookmarkFrecency = Services.prefs.getIntPref(
+ "places.frecency.unvisitedBookmarkBonus"
+ );
+
+ // Add a bookmark and check its frecency.
+ let url = "http://example.com/b";
+ let promiseOne = promiseRankingChanged();
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ await promiseOne;
+
+ let histogram = TelemetryTestUtils.getAndClearHistogram(
+ "PLACES_IDLE_FRECENCY_DECAY_TIME_MS"
+ );
+ info("Trigger frecency decay.");
+ Assert.equal(PlacesUtils.history.isFrecencyDecaying, false);
+ let promiseRanking = promiseRankingChanged();
+
+ PlacesFrecencyRecalculator.observe(null, "idle-daily", "");
+ Assert.equal(PlacesUtils.history.isFrecencyDecaying, true);
+ info("Wait for completion.");
+ await PlacesFrecencyRecalculator.pendingFrecencyDecayPromise;
+
+ await promiseRanking;
+ Assert.equal(PlacesUtils.history.isFrecencyDecaying, false);
+
+ // Now check the new frecency is correct.
+ let newFrecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ { url }
+ );
+
+ Assert.equal(
+ newFrecency,
+ Math.round(unvisitedBookmarkFrecency * PREF_FREC_DECAY_RATE_DEF),
+ "Frecencies should match"
+ );
+
+ let snapshot = histogram.snapshot();
+ Assert.greater(snapshot.sum, 0);
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_observers.js b/toolkit/components/places/tests/unit/test_frecency_observers.js
new file mode 100644
index 0000000000..44747b06f9
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_observers.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Each of these tests a path that triggers a frecency update. Together they
+// hit all sites that update a frecency.
+
+// InsertVisitedURIs::UpdateFrecency and History::InsertPlace
+add_task(
+ async function test_InsertVisitedURIs_UpdateFrecency_and_History_InsertPlace() {
+ // InsertPlace is at the end of a path that UpdateFrecency is also on, so kill
+ // two birds with one stone and expect two notifications. Trigger the path by
+ // adding a download.
+ let url = Services.io.newURI("http://example.com/a");
+ let promise = onRankingChanged();
+ await PlacesUtils.history.insert({
+ url,
+ visits: [
+ {
+ transition: PlacesUtils.history.TRANSITIONS.DOWNLOAD,
+ },
+ ],
+ });
+ await promise;
+ }
+);
+
+// nsNavHistory::UpdateFrecency
+add_task(async function test_nsNavHistory_UpdateFrecency() {
+ let url = Services.io.newURI("http://example.com/b");
+ let promise = onRankingChanged();
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: "test",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ await promise;
+});
+
+// History.jsm invalidateFrecencies()
+add_task(async function test_invalidateFrecencies() {
+ let url = Services.io.newURI("http://test-invalidateFrecencies.com/");
+ // Bookmarking the URI is enough to add it to moz_places, and importantly, it
+ // means that removeByFilter doesn't remove it from moz_places, so its
+ // frecency is able to be changed.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: "test",
+ });
+ let promise = onRankingChanged();
+ await PlacesUtils.history.removeByFilter({ host: url.host });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ await promise;
+});
+
+// History.jsm clear() should not cause a frecency recalculation since pages
+// are removed.
+add_task(async function test_clear() {
+ let received = [];
+ let listener = events =>
+ (received = received.concat(events.map(e => e.type)));
+ PlacesObservers.addListener(
+ ["history-cleared", "pages-rank-changed"],
+ listener
+ );
+ await PlacesUtils.history.clear();
+ PlacesObservers.removeListener(
+ ["history-cleared", "pages-rank-changed"],
+ listener
+ );
+ Assert.deepEqual(received, ["history-cleared"]);
+});
+
+add_task(async function test_nsNavHistory_idleDaily() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://test-site1.org",
+ title: "test",
+ });
+ PlacesFrecencyRecalculator.observe(null, "idle-daily", "");
+ await Promise.all([onRankingChanged()]);
+});
+
+add_task(async function test_nsNavHistory_recalculate() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "https://test-site1.org",
+ title: "test",
+ });
+ await Promise.all([
+ onRankingChanged(),
+ PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies(),
+ ]);
+});
+
+function onRankingChanged() {
+ return PlacesTestUtils.waitForNotification("pages-rank-changed");
+}
diff --git a/toolkit/components/places/tests/unit/test_frecency_origins_alternative.js b/toolkit/components/places/tests/unit/test_frecency_origins_alternative.js
new file mode 100644
index 0000000000..5519149cac
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_origins_alternative.js
@@ -0,0 +1,301 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests alternative origins frecency.
+// Note: the order of the tests here matters, since we are emulating subsquent
+// starts of the recalculator component with different initial conditions.
+
+const FEATURE_PREF = "places.frecency.origins.alternative.featureGate";
+
+async function restartRecalculator() {
+ let subject = {};
+ PlacesFrecencyRecalculator.observe(
+ subject,
+ "test-alternative-frecency-init",
+ ""
+ );
+ await subject.promise;
+}
+
+async function getAllOrigins() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`SELECT * FROM moz_origins`);
+ Assert.greater(rows.length, 0);
+ return rows.map(r => ({
+ host: r.getResultByName("host"),
+ frecency: r.getResultByName("frecency"),
+ recalc_frecency: r.getResultByName("recalc_frecency"),
+ alt_frecency: r.getResultByName("alt_frecency"),
+ recalc_alt_frecency: r.getResultByName("recalc_alt_frecency"),
+ }));
+}
+
+add_setup(async function () {
+ await PlacesTestUtils.addVisits([
+ "https://testdomain1.moz.org",
+ "https://testdomain2.moz.org",
+ "https://testdomain3.moz.org",
+ ]);
+ registerCleanupFunction(PlacesUtils.history.clear);
+});
+
+add_task(async function test_normal_init() {
+ // Ensure moz_meta doesn't report anything.
+ Assert.ok(
+ !PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled,
+ "Check the pref is disabled by default"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.ok(
+ ObjectUtils.isEmpty(
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ Object.create(null)
+ )
+ ),
+ "Check there's no variables stored"
+ );
+});
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, true]],
+ },
+ async function test_enable_init() {
+ // Set alt_frecency to NULL and recalc_alt_frecency = 0 for the entries in
+ // moz_origins to verify they are recalculated.
+ await PlacesTestUtils.updateDatabaseValues("moz_origins", {
+ alt_frecency: null,
+ recalc_alt_frecency: 0,
+ });
+
+ await restartRecalculator();
+
+ // Ensure moz_meta doesn't report anything.
+ Assert.ok(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled,
+ "Check the pref is enabled"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.equal(
+ (
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins
+ .metadataKey,
+ Object.create(null)
+ )
+ ).version,
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables
+ .version,
+ "Check the algorithm version has been stored"
+ );
+
+ // Check all alternative frecencies have been calculated, since we just have
+ // a few.
+ let origins = await getAllOrigins();
+ Assert.ok(
+ origins.every(o => o.recalc_alt_frecency == 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ origins.every(o => o.alt_frecency > 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ }
+);
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, true]],
+ },
+ async function test_different_version() {
+ let origins = await getAllOrigins();
+ Assert.ok(
+ origins.every(o => o.recalc_alt_frecency == 0),
+ "All the entries should not need recalculation"
+ );
+
+ // Set alt_frecency to NULL for the entries in moz_origins to verify they
+ // are recalculated.
+ await PlacesTestUtils.updateDatabaseValues("moz_origins", {
+ alt_frecency: null,
+ });
+
+ // It doesn't matter that the version is, it just have to be different.
+ let variables = await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ Object.create(null)
+ );
+ variables.version = 999;
+ await PlacesUtils.metadata.set(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ variables
+ );
+
+ await restartRecalculator();
+
+ // Check alternative frecency has been marked for recalculation.
+ // Note just after init we reculate a chunk, and this test code is expected
+ // to run before that... though we can't be sure, so if this starts failing
+ // intermittently we'll have to add more synchronization test code.
+ origins = await getAllOrigins();
+ // Ensure moz_meta has been updated.
+ Assert.ok(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled,
+ "Check the pref is enabled"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.deepEqual(
+ (
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins
+ .metadataKey,
+ Object.create(null)
+ )
+ ).version,
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables
+ .version,
+ "Check the algorithm version has been stored"
+ );
+
+ // Check all alternative frecencies have been calculated, since we just have
+ // a few.
+ origins = await getAllOrigins();
+ Assert.ok(
+ origins.every(o => o.recalc_alt_frecency == 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ origins.every(o => o.alt_frecency > 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ }
+);
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, true]],
+ },
+ async function test_different_variables() {
+ let origins = await getAllOrigins();
+ Assert.ok(
+ origins.every(o => o.recalc_alt_frecency == 0),
+ "All the entries should not need recalculation"
+ );
+
+ // Set alt_frecency to NULL for the entries in moz_origins to verify they
+ // are recalculated.
+ await PlacesTestUtils.updateDatabaseValues("moz_origins", {
+ alt_frecency: null,
+ });
+
+ // Change variables.
+ let variables = await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ Object.create(null)
+ );
+ Assert.greater(Object.keys(variables).length, 1);
+ Assert.ok("version" in variables, "At least the version is always present");
+ await PlacesUtils.metadata.set(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ {
+ version:
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables
+ .version,
+ someVar: 1,
+ }
+ );
+
+ await restartRecalculator();
+
+ // Ensure moz_meta has been updated.
+ Assert.ok(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.enabled,
+ "Check the pref is enabled"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.deepEqual(
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ Object.create(null)
+ ),
+ variables,
+ "Check the algorithm variables have been stored"
+ );
+
+ // Check all alternative frecencies have been calculated, since we just have
+ // a few.
+ origins = await getAllOrigins();
+ Assert.ok(
+ origins.every(o => o.recalc_alt_frecency == 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ origins.every(o => o.alt_frecency > 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ }
+);
+
+add_task(async function test_disable() {
+ let origins = await getAllOrigins();
+ Assert.ok(
+ origins.every(o => o.recalc_alt_frecency == 0),
+ "All the entries should not need recalculation"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.equal(
+ (
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ Object.create(null)
+ )
+ ).version,
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.variables
+ .version,
+ "Check the algorithm version has been stored"
+ );
+
+ await restartRecalculator();
+
+ // Check alternative frecency has not been marked for recalculation.
+ origins = await getAllOrigins();
+ Assert.ok(
+ origins.every(o => o.recalc_alt_frecency == 0),
+ "The entries not have been marked for recalc"
+ );
+ Assert.ok(
+ origins.every(o => o.alt_frecency === null),
+ "All the alt_frecency values should have been nullified"
+ );
+
+ // Ensure moz_meta has been updated.
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.ok(
+ ObjectUtils.isEmpty(
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.origins.metadataKey,
+ Object.create(null)
+ )
+ ),
+ "Check the algorithm variables has been removed"
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_origins_recalc.js b/toolkit/components/places/tests/unit/test_frecency_origins_recalc.js
new file mode 100644
index 0000000000..3a93912f13
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_origins_recalc.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that recalc_frecency in the moz_origins table works is consistent.
+
+add_task(async function test() {
+ // test recalc_frecency is set to 1 when frecency of a page changes.
+ // Add a couple visits, then remove one of them.
+ const now = new Date();
+ const host = "mozilla.org";
+ const url = `https://${host}/test/`;
+ await PlacesTestUtils.addVisits([
+ {
+ url,
+ visitDate: now,
+ },
+ {
+ url,
+ visitDate: new Date(new Date().setDate(now.getDate() - 30)),
+ },
+ ]);
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_origins", "recalc_frecency", {
+ host,
+ }),
+ 1,
+ "Frecency should be calculated"
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue(
+ "moz_origins",
+ "recalc_alt_frecency",
+ {
+ host,
+ }
+ ),
+ 1,
+ "Alt frecency should be calculated"
+ );
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let alt_frecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_origins",
+ "alt_frecency",
+ { host }
+ );
+ let frecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_origins",
+ "frecency",
+ { host }
+ );
+ // Remove only one visit (otherwise the page would be orphaned).
+ await PlacesUtils.history.removeVisitsByFilter({
+ beginDate: new Date(now.valueOf() - 10000),
+ endDate: new Date(now.valueOf() + 10000),
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_origins", "recalc_frecency", {
+ host,
+ }),
+ 0,
+ "Should have been calculated"
+ );
+ Assert.greater(
+ frecency,
+ await PlacesTestUtils.getDatabaseValue("moz_origins", "frecency", {
+ host,
+ }),
+ "frecency should have decreased"
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue(
+ "moz_origins",
+ "recalc_alt_frecency",
+ { host }
+ ),
+ 0,
+ "Should have been calculated"
+ );
+ Assert.greater(
+ alt_frecency,
+ await PlacesTestUtils.getDatabaseValue("moz_origins", "alt_frecency", {
+ host,
+ }),
+ "alternative frecency should have decreased"
+ );
+
+ // Add another page to the same host.
+ const url2 = `https://${host}/second/`;
+ await PlacesTestUtils.addVisits(url2);
+ // Remove the first page.
+ await PlacesUtils.history.remove(url);
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_origins", "recalc_frecency", {
+ host,
+ }),
+ 1,
+ "Frecency should be calculated"
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue(
+ "moz_origins",
+ "recalc_alt_frecency",
+ { host }
+ ),
+ 1,
+ "Alt frecency should be calculated"
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_pages_alternative.js b/toolkit/components/places/tests/unit/test_frecency_pages_alternative.js
new file mode 100644
index 0000000000..0ea271c698
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_pages_alternative.js
@@ -0,0 +1,364 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests alternative pages frecency.
+// Note: the order of the tests here matters, since we are emulating subsquent
+// starts of the recalculator component with different initial conditions.
+
+const FEATURE_PREF = "places.frecency.pages.alternative.featureGate";
+
+async function restartRecalculator() {
+ let subject = {};
+ PlacesFrecencyRecalculator.observe(
+ subject,
+ "test-alternative-frecency-init",
+ ""
+ );
+ await subject.promise;
+}
+
+async function getAllPages() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`SELECT * FROM moz_places`);
+ Assert.greater(rows.length, 0);
+ return rows.map(r => ({
+ url: r.getResultByName("url"),
+ frecency: r.getResultByName("frecency"),
+ recalc_frecency: r.getResultByName("recalc_frecency"),
+ alt_frecency: r.getResultByName("alt_frecency"),
+ recalc_alt_frecency: r.getResultByName("recalc_alt_frecency"),
+ }));
+}
+
+add_setup(async function () {
+ await PlacesTestUtils.addVisits([
+ "https://testdomain1.moz.org",
+ "https://testdomain2.moz.org",
+ "https://testdomain3.moz.org",
+ ]);
+ registerCleanupFunction(PlacesUtils.history.clear);
+});
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, false]],
+ },
+ async function test_normal_init() {
+ // The test starts with the pref enabled, otherwise we'd not have the SQL
+ // function defined. So here we disable it, then enable again later.
+ await restartRecalculator();
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.ok(
+ ObjectUtils.isEmpty(
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ )
+ ),
+ "Check there's no variables stored"
+ );
+ }
+);
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, true]],
+ },
+ async function test_enable_init() {
+ // Set alt_frecency to NULL and recalc_alt_frecency = 0 for the entries in
+ // moz_places to verify they are recalculated.
+ await PlacesTestUtils.updateDatabaseValues("moz_places", {
+ alt_frecency: null,
+ recalc_alt_frecency: 0,
+ });
+
+ await restartRecalculator();
+
+ // Ensure moz_meta doesn't report anything.
+ Assert.ok(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled,
+ "Check the pref is enabled"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.equal(
+ (
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ )
+ ).version,
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables
+ .version,
+ "Check the algorithm version has been stored"
+ );
+
+ // Check all alternative frecencies have been calculated, since we just have
+ // a few.
+ let pages = await getAllPages();
+ Assert.ok(
+ pages.every(p => p.recalc_alt_frecency == 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ pages.every(p => p.alt_frecency > 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ }
+);
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, true]],
+ },
+ async function test_different_version() {
+ let pages = await getAllPages();
+ Assert.ok(
+ pages.every(p => p.recalc_alt_frecency == 0),
+ "All the entries should not need recalculation"
+ );
+
+ // Set alt_frecency to NULL to verify all the entries are recalculated.
+ await PlacesTestUtils.updateDatabaseValues("moz_places", {
+ alt_frecency: null,
+ });
+
+ // It doesn't matter that the version is, it just have to be different.
+ let variables = await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ );
+ variables.version = 999;
+ await PlacesUtils.metadata.set(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ variables
+ );
+
+ await restartRecalculator();
+
+ // Check alternative frecency has been marked for recalculation.
+ // Note just after init we reculate a chunk, and this test code is expected
+ // to run before that... though we can't be sure, so if this starts failing
+ // intermittently we'll have to add more synchronization test code.
+ pages = await getAllPages();
+ // Ensure moz_meta has been updated.
+ Assert.ok(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled,
+ "Check the pref is enabled"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.deepEqual(
+ (
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ )
+ ).version,
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables
+ .version,
+ "Check the algorithm version has been stored"
+ );
+
+ // Check all alternative frecencies have been calculated, since we just have
+ // a few.
+ pages = await getAllPages();
+ Assert.ok(
+ pages.every(p => p.recalc_alt_frecency == 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ pages.every(p => p.alt_frecency > 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ }
+);
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, true]],
+ },
+ async function test_different_variables() {
+ let pages = await getAllPages();
+ Assert.ok(
+ pages.every(p => p.recalc_alt_frecency == 0),
+ "All the entries should not need recalculation"
+ );
+
+ // Set alt_frecency to NULL to verify all the entries are recalculated.
+ await PlacesTestUtils.updateDatabaseValues("moz_places", {
+ alt_frecency: null,
+ });
+
+ // Change variables.
+ let variables = await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ );
+ Assert.greater(Object.keys(variables).length, 1);
+ Assert.ok("version" in variables, "At least the version is always present");
+ await PlacesUtils.metadata.set(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ {
+ version:
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables
+ .version,
+ someVar: 1,
+ }
+ );
+
+ await restartRecalculator();
+
+ // Ensure moz_meta has been updated.
+ Assert.ok(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.enabled,
+ "Check the pref is enabled"
+ );
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.deepEqual(
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ ),
+ variables,
+ "Check the algorithm variables have been stored"
+ );
+
+ // Check all alternative frecencies have been calculated, since we just have
+ // a few.
+ pages = await getAllPages();
+ Assert.ok(
+ pages.every(p => p.recalc_alt_frecency == 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ pages.every(p => p.alt_frecency > 0),
+ "All the entries have been recalculated"
+ );
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ }
+);
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, false]],
+ },
+ async function test_disable() {
+ let pages = await getAllPages();
+ Assert.ok(
+ pages.every(p => p.recalc_alt_frecency == 0),
+ "All the entries should not need recalculation"
+ );
+
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.equal(
+ (
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ )
+ ).version,
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.variables
+ .version,
+ "Check the algorithm version has been stored"
+ );
+
+ await restartRecalculator();
+
+ // Check alternative frecency has not been marked for recalculation.
+ pages = await getAllPages();
+ Assert.ok(
+ pages.every(p => p.recalc_alt_frecency == 0),
+ "The entries not have been marked for recalc"
+ );
+ Assert.ok(
+ pages.every(p => p.alt_frecency === null),
+ "All the alt_frecency values should have been nullified"
+ );
+
+ // Ensure moz_meta has been updated.
+ // Avoid hitting the cache, we want to check the actual database value.
+ PlacesUtils.metadata.cache.clear();
+ Assert.ok(
+ ObjectUtils.isEmpty(
+ await PlacesUtils.metadata.get(
+ PlacesFrecencyRecalculator.alternativeFrecencyInfo.pages.metadataKey,
+ Object.create(null)
+ )
+ ),
+ "Check the algorithm variables has been removed"
+ );
+ }
+);
+
+add_task(
+ {
+ pref_set: [[FEATURE_PREF, true]],
+ },
+ async function test_score() {
+ await restartRecalculator();
+
+ // This is not intended to cover the algorithm as a whole, but just as a
+ // sanity check for scores.
+
+ // Add before visits to properly set visit source.
+ await PlacesUtils.bookmarks.insert({
+ url: "https://visitedbookmark.moz.org",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+
+ await PlacesTestUtils.addVisits([
+ {
+ url: "https://low.moz.org",
+ transition: PlacesUtils.history.TRANSITIONS.FRAMED_LINK,
+ },
+ {
+ url: "https://visitedbookmark.moz.org",
+ transition: PlacesUtils.history.TRANSITIONS.FRAMED_LINK,
+ },
+ {
+ url: "https://old.moz.org",
+ visitDate: (Date.now() - 2 * 86400000) * 1000,
+ },
+ { url: "https://base.moz.org" },
+ { url: "https://manyvisits.moz.org" },
+ { url: "https://manyvisits.moz.org" },
+ { url: "https://manyvisits.moz.org" },
+ { url: "https://manyvisits.moz.org" },
+ ]);
+ await PlacesUtils.bookmarks.insert({
+ url: "https://unvisitedbookmark.moz.org",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ let getFrecency = url =>
+ PlacesTestUtils.getDatabaseValue("moz_places", "alt_frecency", {
+ url,
+ });
+ let low = await getFrecency("https://low.moz.org/");
+ let old = await getFrecency("https://old.moz.org/");
+ Assert.greater(old, low);
+ let base = await getFrecency("https://base.moz.org/");
+ Assert.greater(base, old);
+ let unvisitedBm = await getFrecency("https://unvisitedbookmark.moz.org/");
+ Assert.greater(unvisitedBm, base);
+ let manyVisits = await getFrecency("https://manyvisits.moz.org/");
+ Assert.greater(manyVisits, unvisitedBm);
+ let visitedBm = await getFrecency("https://visitedbookmark.moz.org/");
+ Assert.greater(visitedBm, base);
+ }
+);
diff --git a/toolkit/components/places/tests/unit/test_frecency_pages_recalc_alt.js b/toolkit/components/places/tests/unit/test_frecency_pages_recalc_alt.js
new file mode 100644
index 0000000000..9642e87ad2
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_pages_recalc_alt.js
@@ -0,0 +1,97 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that recalc_alt_frecency in the moz_places table is updated correctly.
+
+add_task(async function test() {
+ // Start from a stable situation, otherwise the recalculator may set recalc
+ // field at any time due to the initialization.
+ let subject = {};
+ PlacesFrecencyRecalculator.observe(
+ subject,
+ "test-alternative-frecency-init",
+ ""
+ );
+ await subject.promise;
+
+ info("test recalc_alt_frecency is set to 1 when a visit is added");
+ const now = new Date();
+ const URL = "https://mozilla.org/test/";
+ let getRecalc = url =>
+ PlacesTestUtils.getDatabaseValue("moz_places", "recalc_alt_frecency", {
+ url,
+ });
+ let setRecalc = (url, val) =>
+ PlacesTestUtils.updateDatabaseValues(
+ "moz_places",
+ { recalc_alt_frecency: val },
+ { url }
+ );
+ let getFrecency = url =>
+ PlacesTestUtils.getDatabaseValue("moz_places", "alt_frecency", {
+ url,
+ });
+ await PlacesTestUtils.addVisits([
+ {
+ url: URL,
+ visitDate: now,
+ },
+ {
+ url: URL,
+ visitDate: new Date(new Date().setDate(now.getDate() - 30)),
+ },
+ ]);
+ // The frecency has been recalculated by addVisits already, checking that
+ // alt frecency has a positive value should be sufficient anyway.
+ Assert.equal(await getRecalc(URL), 0);
+ Assert.greater(await getFrecency(URL), 0);
+
+ info("Remove just one visit (otherwise the page would be orphaned).");
+ await PlacesUtils.history.removeVisitsByFilter({
+ beginDate: new Date(now.valueOf() - 10000),
+ endDate: new Date(now.valueOf() + 10000),
+ });
+ Assert.equal(await getRecalc(URL), 1);
+ await setRecalc(URL, 0);
+
+ info("Add a bookmark to the page");
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: URL,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ Assert.equal(await getRecalc(URL), 1);
+ await setRecalc(URL, 0);
+
+ info("Clear history");
+ await PlacesUtils.history.clear();
+ Assert.equal(await getRecalc(URL), 1);
+ await setRecalc(URL, 0);
+
+ // Add back a visit so the page is not an orphan once we remove the bookmark.
+ await PlacesTestUtils.addVisits(URL);
+ Assert.equal(await getRecalc(URL), 0);
+ Assert.greater(await getFrecency(URL), 0);
+
+ info("change the bookmark URL");
+ const URL2 = "https://editedbookmark.org/";
+ bm.url = URL2;
+ await PlacesUtils.bookmarks.update(bm);
+ Assert.equal(await getRecalc(URL), 1);
+ Assert.equal(await getRecalc(URL2), 1);
+ await setRecalc(URL, 0);
+ await setRecalc(URL2, 0);
+
+ info("Remove the bookmark from the page");
+ await PlacesUtils.bookmarks.remove(bm);
+ Assert.equal(await getRecalc(URL2), 1);
+ await setRecalc(URL2, 0);
+
+ const URL3 = "https://test3.moz.org/";
+ const URL4 = "https://test4.moz.org/";
+ info("Insert multiple pages now");
+ await PlacesTestUtils.addVisits([URL3, URL4]);
+ Assert.equal(await getRecalc(URL3), 0);
+ Assert.greater(await getFrecency(URL3), 0);
+ Assert.equal(await getRecalc(URL4), 0);
+ Assert.greater(await getFrecency(URL4), 0);
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_recalc_triggers.js b/toolkit/components/places/tests/unit/test_frecency_recalc_triggers.js
new file mode 100644
index 0000000000..2e75a6d459
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_recalc_triggers.js
@@ -0,0 +1,281 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that all the operations affecting frecency are either recalculating
+ * immediately or triggering a recalculation.
+ * Operations that should recalculate immediately:
+ * - adding visits
+ * Operations that should just trigger a recalculation:
+ * - removing visits
+ * - adding a bookmark
+ * - removing a bookmark
+ * - changing url of a bookmark
+ *
+ * Also check setting a frecency resets recalc_frecency to 0.
+ **/
+
+const TEST_URL = "https://example.com/";
+const TEST_URL_2 = "https://example2.com/";
+
+// NOTE: Until we fix Bug 1806666 this test has to run queries manually because
+// the official APIs recalculate frecency immediately. After the fix, these
+// helpers can be removed and the test can be much simpler.
+function insertVisit(url) {
+ return PlacesUtils.withConnectionWrapper("insertVisit", async db => {
+ await db.execute(
+ `INSERT INTO moz_historyvisits(place_id, visit_date, visit_type)
+ VALUES ((SELECT id FROM moz_places WHERE url = :url), 1648226608386000, 1)`,
+ { url }
+ );
+ });
+}
+function removeVisit(url) {
+ return PlacesUtils.withConnectionWrapper("insertVisit", async db => {
+ await db.execute(
+ `DELETE FROM moz_historyvisits WHERE place_id
+ = (SELECT id FROM moz_places WHERE url = :url)`,
+ { url }
+ );
+ });
+}
+function resetFrecency(url) {
+ return PlacesUtils.withConnectionWrapper("insertVisit", async db => {
+ await db.execute(`UPDATE moz_places SET frecency = -1 WHERE url = :url`, {
+ url,
+ });
+ });
+}
+
+add_task(async function test_visit() {
+ // First add a bookmark so the page is not orphaned.
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: TEST_URL,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+
+ info("Add a visit check frecency is calculated immediately");
+ await PlacesTestUtils.addVisits(TEST_URL);
+ let originalFrecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.greater(originalFrecency, 0, "frecency was recalculated immediately");
+ let recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 0, "frecency doesn't need a recalc");
+
+ info("Add a visit (raw query) check frecency is not calculated immediately");
+ await insertVisit(TEST_URL);
+ let frecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(frecency, originalFrecency, "frecency is unchanged");
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 1, "frecency needs a recalc");
+
+ info("Check setting frecency resets recalc_frecency");
+ await resetFrecency(TEST_URL);
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 0, "frecency doesn't need a recalc");
+
+ info("Removing a visit sets recalc_frecency");
+ await removeVisit(TEST_URL);
+ frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL,
+ });
+ Assert.equal(frecency, -1, "frecency is unchanged");
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 1, "frecency needs a recalc");
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.remove(bm);
+});
+
+add_task(async function test_bookmark() {
+ // First add a visit so the page is not orphaned.
+ await PlacesTestUtils.addVisits([TEST_URL, TEST_URL_2]);
+
+ let originalFrecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.greater(originalFrecency, 0);
+
+ info("Check adding a bookmark sets recalc_frecency");
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: TEST_URL,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+
+ let frecency = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(frecency, originalFrecency, "frecency is unchanged");
+ let recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 1, "frecency needs a recalc");
+
+ info("Check changing a bookmark url sets recalc_frecency on both urls");
+ await PlacesUtils.bookmarks.update({
+ guid: bm.guid,
+ url: TEST_URL_2,
+ });
+ frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL,
+ });
+ Assert.equal(frecency, originalFrecency, "frecency is unchanged");
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 1, "frecency needs a recalc");
+ frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL_2,
+ });
+ Assert.ok(frecency > 0, "frecency is valid");
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL_2,
+ }
+ );
+ Assert.equal(recalc, 1, "frecency needs a recalc");
+
+ info("Check setting frecency resets recalc_frecency");
+ await resetFrecency(TEST_URL);
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 0, "frecency doesn't need a recalc");
+ await resetFrecency(TEST_URL_2);
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL_2,
+ }
+ );
+ Assert.equal(recalc, 0, "frecency doesn't need a recalc");
+
+ info("Removing a bookmark sets recalc_frecency");
+ await PlacesUtils.bookmarks.remove(bm.guid);
+ frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL,
+ });
+ Assert.equal(frecency, -1, "frecency is unchanged");
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL,
+ }
+ );
+ Assert.equal(recalc, 0, "frecency doesn't need a recalc");
+ frecency = await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URL_2,
+ });
+ Assert.equal(frecency, -1, "frecency is unchanged");
+ recalc = await PlacesTestUtils.getDatabaseValue(
+ "moz_places",
+ "recalc_frecency",
+ {
+ url: TEST_URL_2,
+ }
+ );
+ Assert.equal(recalc, 1, "frecency needs a recalc");
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_bookmark_frecency_zero() {
+ info("A url with frecency 0 should be recalculated if bookmarked");
+ let url = "https://zerofrecency.org/";
+ await PlacesTestUtils.addVisits({ url, transition: TRANSITION_FRAMED_LINK });
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url }),
+ 0
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "recalc_frecency", {
+ url,
+ }),
+ 0
+ );
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "recalc_frecency", {
+ url,
+ }),
+ 1
+ );
+ info("place: uris should not be recalculated");
+ url = "place:test";
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url }),
+ 0
+ );
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "recalc_frecency", {
+ url,
+ }),
+ 0
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_recalculator.js b/toolkit/components/places/tests/unit/test_frecency_recalculator.js
new file mode 100644
index 0000000000..ed4501c56a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_recalculator.js
@@ -0,0 +1,178 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test PlacesFrecencyRecalculator scheduling.
+
+// Enable the collection (during test) for all products so even products
+// that don't collect the data will be able to run the test without failure.
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+async function getOriginFrecency(origin) {
+ let db = await PlacesUtils.promiseDBConnection();
+ return (
+ await db.execute(
+ `SELECT frecency
+ FROM moz_origins
+ WHERE host = :origin`,
+ { origin }
+ )
+ )[0].getResultByIndex(0);
+}
+
+async function resetOriginFrecency(origin) {
+ await PlacesUtils.withConnectionWrapper(
+ "test_frecency_recalculator reset origin",
+ async db => {
+ await db.executeCached(
+ `UPDATE moz_origins
+ SET frecency = -1
+ WHERE host = :origin`,
+ { origin }
+ );
+ }
+ );
+}
+
+async function addVisitsAndSetRecalc(urls) {
+ await PlacesTestUtils.addVisits(urls);
+ await PlacesUtils.withConnectionWrapper(
+ "test_frecency_recalculator set recalc",
+ async db => {
+ await db.executeCached(
+ `UPDATE moz_places
+ SET frecency = -1
+ WHERE url in (
+ ${PlacesUtils.sqlBindPlaceholders(urls)}
+ )`,
+ urls
+ );
+ await db.executeCached(
+ `UPDATE moz_places
+ SET recalc_frecency = (CASE WHEN url in (
+ ${PlacesUtils.sqlBindPlaceholders(urls)}
+ ) THEN 1 ELSE 0 END)`,
+ urls
+ );
+ }
+ );
+}
+
+add_task(async function test() {
+ info("On startup a recalculation is always pending.");
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ // If everything gets recalculated, then it should not be pending anymore.
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.ok(
+ !PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should not be pending"
+ );
+
+ // If after a recalculation there's outdated entries left, a new recalculation
+ // should be pending.
+ info("Insert outdated frecencies");
+ const url1 = new URL("https://test1.moz.org/");
+ const url2 = new URL("https://test2.moz.org/");
+ await addVisitsAndSetRecalc([url1.href, url2.href]);
+ await resetOriginFrecency(url1.host);
+ await resetOriginFrecency(url2.host);
+
+ Assert.ok(
+ PlacesUtils.history.shouldStartFrecencyRecalculation,
+ "Should have set shouldStartFrecencyRecalculation"
+ );
+ await PlacesFrecencyRecalculator.recalculateSomeFrecencies({ chunkSize: 1 });
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ // Recalculating uri1 will set its origin to recalc, that means there's 2
+ // origins to recalc now. Passing chunkSize: 2 here would then retrigger the
+ // recalc, thinking we saturated the chunk, thus we use 3.
+ await PlacesFrecencyRecalculator.recalculateSomeFrecencies({ chunkSize: 3 });
+ Assert.ok(
+ !PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should not be pending"
+ );
+ Assert.ok(
+ !PlacesUtils.history.shouldStartFrecencyRecalculation,
+ "Should have unset shouldStartFrecencyRecalculation"
+ );
+
+ Assert.greater(await getOriginFrecency(url1.host), 0);
+ Assert.greater(await getOriginFrecency(url2.host), 0);
+
+ info("Changing recalc_frecency of an entry adds a pending recalculation.");
+ PlacesUtils.history.shouldStartFrecencyRecalculation = false;
+ let promiseNotify = TestUtils.topicObserved("frecency-recalculation-needed");
+ await PlacesUtils.withConnectionWrapper(
+ "test_frecency_recalculator",
+ async db => {
+ await db.executeCached(
+ `UPDATE moz_places SET recalc_frecency = 1 WHERE url = :url`,
+ { url: url1.href }
+ );
+ }
+ );
+ Assert.ok(
+ PlacesUtils.history.shouldStartFrecencyRecalculation,
+ "Should have set shouldStartFrecencyRecalculation"
+ );
+
+ await promiseNotify;
+ Assert.ok(
+ PlacesFrecencyRecalculator.isRecalculationPending,
+ "Recalculation should be pending"
+ );
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.ok(
+ !PlacesUtils.history.shouldStartFrecencyRecalculation,
+ "Should have unset shouldStartFrecencyRecalculation"
+ );
+});
+
+add_task(async function test_chunk_time_telemetry() {
+ await PlacesUtils.bookmarks.insert({
+ url: "https://test-bookmark.com",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ Assert.ok(
+ PlacesUtils.history.shouldStartFrecencyRecalculation,
+ "Should have set shouldStartFrecencyRecalculation"
+ );
+ let histogram = TelemetryTestUtils.getAndClearHistogram(
+ "PLACES_FRECENCY_RECALC_CHUNK_TIME_MS"
+ );
+ let subject = {};
+ PlacesFrecencyRecalculator.observe(subject, "test-execute-taskFn", "");
+ await subject.promise;
+ let snapshot = histogram.snapshot();
+ Assert.equal(
+ Object.values(snapshot.values).reduce((a, b) => a + b, 0),
+ 1
+ );
+ Assert.greater(snapshot.sum, 0);
+ Assert.ok(
+ !PlacesUtils.history.shouldStartFrecencyRecalculation,
+ "Should have unset shouldStartFrecencyRecalculation"
+ );
+
+ // It should now not report any new time, since there's nothing to recalculate.
+ histogram.clear();
+ PlacesFrecencyRecalculator.observe(subject, "test-execute-taskFn", "");
+ await subject.promise;
+ snapshot = histogram.snapshot();
+ Assert.equal(
+ Object.values(snapshot.values).reduce((a, b) => a + b, 0),
+ 0
+ );
+ Assert.ok(
+ !PlacesUtils.history.shouldStartFrecencyRecalculation,
+ "Should still not have set shouldStartFrecencyRecalculation"
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_unvisited_bookmark.js b/toolkit/components/places/tests/unit/test_frecency_unvisited_bookmark.js
new file mode 100644
index 0000000000..ae7bd3d813
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_unvisited_bookmark.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests frecency of unvisited bookmarks.
+
+add_task(async function () {
+ // Randomly sorted by date.
+ const now = new Date();
+ const bookmarks = [
+ {
+ url: "https://example.com/1",
+ date: new Date(new Date().setDate(now.getDate() - 30)),
+ },
+ {
+ url: "https://example.com/2",
+ date: new Date(new Date().setDate(now.getDate() - 1)),
+ },
+ {
+ url: "https://example.com/3",
+ date: new Date(new Date().setDate(now.getDate() - 100)),
+ },
+ {
+ url: "https://example.com/1", // Same url but much older.
+ date: new Date(new Date().setDate(now.getDate() - 120)),
+ },
+ ];
+
+ for (let bookmark of bookmarks) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: bookmark.url,
+ dateAdded: bookmark.date,
+ });
+ }
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ // The newest bookmark should have an higher frecency.
+ Assert.greater(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: bookmarks[1].url,
+ }),
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: bookmarks[0].url,
+ })
+ );
+ Assert.greater(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: bookmarks[0].url,
+ }),
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: bookmarks[2].url,
+ })
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency_zero_updated.js b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js
new file mode 100644
index 0000000000..44c329635e
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests a zero frecency is correctly updated when inserting new valid visits.
+
+add_task(async function () {
+ const TEST_URI = NetUtil.newURI("http://example.com/");
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URI,
+ title: "A title",
+ });
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.ok(
+ (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ })) > 0
+ );
+
+ // Removing the bookmark should leave an orphan page with zero frecency.
+ // Note this would usually be expired later by expiration.
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ // Now add a valid visit to the page, frecency should increase.
+ await PlacesTestUtils.addVisits({ uri: TEST_URI });
+ Assert.ok(
+ (await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ })) > 0
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_getChildIndex.js b/toolkit/components/places/tests/unit/test_getChildIndex.js
new file mode 100644
index 0000000000..35eb6fd22b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_getChildIndex.js
@@ -0,0 +1,73 @@
+/* -*- 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/. */
+
+/*
+ * Tests nsNavHistoryContainerResultNode::GetChildIndex(aNode) functionality.
+ */
+
+add_task(async function test_get_child_index() {
+ // Add a bookmark to the menu.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://test.mozilla.org/bookmark/",
+ title: "Test bookmark",
+ });
+
+ // Add a bookmark to unfiled folder.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://test.mozilla.org/unfiled/",
+ title: "Unfiled bookmark",
+ });
+
+ // Get the unfiled bookmark node.
+ let unfiledNode = getNodeAt(PlacesUtils.bookmarks.unfiledGuid, 0);
+ if (!unfiledNode) {
+ do_throw("Unable to find bookmark in hierarchy!");
+ }
+ Assert.equal(unfiledNode.title, "Unfiled bookmark");
+
+ let hs = PlacesUtils.history;
+ let query = hs.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.menuGuid]);
+ let options = hs.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = hs.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ // Check functionality for proper nodes.
+ for (let i = 0; i < root.childCount; i++) {
+ let node = root.getChild(i);
+ print("Now testing: " + node.title);
+ Assert.equal(root.getChildIndex(node), i);
+ }
+
+ // Now search for an invalid node and expect an exception.
+ try {
+ root.getChildIndex(unfiledNode);
+ do_throw("Searching for an invalid node should have thrown.");
+ } catch (ex) {
+ print("We correctly got an exception.");
+ }
+
+ root.containerOpen = false;
+});
+
+function getNodeAt(aFolderGuid, aIndex) {
+ let hs = PlacesUtils.history;
+ let query = hs.getNewQuery();
+ query.setParents([aFolderGuid]);
+ let options = hs.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = hs.executeQuery(query, options).root;
+ root.containerOpen = true;
+ if (root.childCount < aIndex) {
+ do_throw("Not enough children to find bookmark!");
+ }
+ let node = root.getChild(aIndex);
+ root.containerOpen = false;
+ return node;
+}
diff --git a/toolkit/components/places/tests/unit/test_get_query_param_sql_function.js b/toolkit/components/places/tests/unit/test_get_query_param_sql_function.js
new file mode 100644
index 0000000000..072f53ceac
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_get_query_param_sql_function.js
@@ -0,0 +1,21 @@
+add_task(async function test_get_query_param_sql_function() {
+ let db = await PlacesUtils.promiseDBConnection();
+ await Assert.rejects(
+ db.execute(`SELECT get_query_param()`),
+ /wrong number of arguments/
+ );
+ let rows = await db.execute(`SELECT
+ get_query_param('a=b&c=d', 'a'),
+ get_query_param('a=b&c=d', 'c'),
+ get_query_param('a=b&a=c', 'a'),
+ get_query_param('a=b&c=d', 'e'),
+ get_query_param('a', 'a'),
+ get_query_param(NULL, NULL),
+ get_query_param('a=b&c=d', NULL),
+ get_query_param(NULL, 'a')`);
+ let results = ["b", "d", "b", null, "", null, null, null];
+ equal(rows[0].numEntries, results.length);
+ for (let i = 0; i < results.length; ++i) {
+ equal(rows[0].getResultByIndex(i), results[i]);
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_hash.js b/toolkit/components/places/tests/unit/test_hash.js
new file mode 100644
index 0000000000..701cb3d151
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_hash.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function () {
+ // Check particular unicode urls with insertion and selection APIs to ensure
+ // url hashes match properly.
+ const URLS = [
+ "http://президент.президент/президент/",
+ "https://www.аррӏе.com/аррӏе/",
+ "http://名がドメイン/",
+ ];
+
+ for (let url of URLS) {
+ await PlacesTestUtils.addVisits(url);
+ Assert.ok(await PlacesUtils.history.fetch(url), "Found the added visit");
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch({ url }),
+ "Found the added bookmark"
+ );
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(
+ "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url",
+ { url: new URL(url).href }
+ );
+ Assert.equal(rows.length, 1, "Matched the place from the database");
+ let id = rows[0].getResultByName("id");
+
+ // Now, suppose the urls has been inserted without proper parsing and retry.
+ // This should normally not happen through the API, but we have evidence
+ // it somehow happened.
+ await PlacesUtils.withConnectionWrapper("test_hash.js", async wdb => {
+ await wdb.execute(
+ `
+ UPDATE moz_places SET url_hash = hash(:url), url = :url
+ WHERE id = :id
+ `,
+ { url, id }
+ );
+ rows = await wdb.execute(
+ "SELECT id FROM moz_places WHERE url_hash = hash(:url) AND url = :url",
+ { url }
+ );
+ Assert.equal(rows.length, 1, "Matched the place from the database");
+ });
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_history.js b/toolkit/components/places/tests/unit/test_history.js
new file mode 100644
index 0000000000..3ea3696317
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history.js
@@ -0,0 +1,158 @@
+/* -*- 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 history services
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+);
+
+/**
+ * Checks to see that a URI is in the database.
+ *
+ * @param aURI
+ * The URI to check.
+ * @returns true if the URI is in the DB, false otherwise.
+ */
+function uri_in_db(aURI) {
+ var options = histsvc.getNewQueryOptions();
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_URI;
+ var query = histsvc.getNewQuery();
+ query.uri = aURI;
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ root.containerOpen = false;
+ return cc == 1;
+}
+
+// main
+
+add_task(async function test_execute() {
+ // we have a new profile, so we should have imported bookmarks
+ Assert.equal(histsvc.databaseStatus, histsvc.DATABASE_STATUS_CREATE);
+
+ // add a visit
+ var testURI = uri("http://mozilla.com");
+ await PlacesTestUtils.addVisits(testURI);
+
+ // now query for the visit, setting sorting and limit such that
+ // we should retrieve only the visit we just added
+ var options = histsvc.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.maxResults = 1;
+ options.resultType = options.RESULTS_AS_VISIT;
+ var query = histsvc.getNewQuery();
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+ for (var i = 0; i < cc; ++i) {
+ var node = root.getChild(i);
+ // test node properties in RESULTS_AS_VISIT
+ Assert.equal(node.uri, testURI.spec);
+ Assert.equal(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+
+ // add another visit for the same URI, and a third visit for a different URI
+ var testURI2 = uri("http://google.com/");
+ await PlacesTestUtils.addVisits(testURI);
+ await PlacesTestUtils.addVisits(testURI2);
+
+ options.maxResults = 5;
+ options.resultType = options.RESULTS_AS_URI;
+
+ // test minVisits
+ query.minVisits = 0;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.minVisits = 1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.minVisits = 2;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 1);
+ query.minVisits = 3;
+ result.root.containerOpen = false;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 0);
+ result.root.containerOpen = false;
+
+ // test maxVisits
+ query.minVisits = -1;
+ query.maxVisits = -1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.maxVisits = 0;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 0);
+ result.root.containerOpen = false;
+ query.maxVisits = 1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 1);
+ result.root.containerOpen = false;
+ query.maxVisits = 2;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.maxVisits = 3;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ Assert.equal(result.root.childCount, 2);
+ result.root.containerOpen = false;
+
+ // By default history is enabled.
+ Assert.ok(!histsvc.historyDisabled);
+
+ // test getPageTitle
+ await PlacesTestUtils.addVisits({
+ uri: uri("http://example.com"),
+ title: "title",
+ });
+ let placeInfo = await PlacesUtils.history.fetch("http://example.com");
+ Assert.equal(placeInfo.title, "title");
+
+ // query for the visit
+ Assert.ok(uri_in_db(testURI));
+
+ // test for schema changes in bug 373239
+ // get direct db connection
+ var db = histsvc.DBConnection;
+ var q = "SELECT id FROM moz_bookmarks";
+ var statement;
+ try {
+ statement = db.createStatement(q);
+ } catch (ex) {
+ do_throw("bookmarks table does not have id field, schema is too old!");
+ } finally {
+ statement.finalize();
+ }
+
+ // bug 394741 - regressed history text searches
+ await PlacesTestUtils.addVisits(uri("http://mozilla.com"));
+ options = histsvc.getNewQueryOptions();
+ // options.resultType = options.RESULTS_AS_VISIT;
+ query = histsvc.getNewQuery();
+ query.searchTerms = "moz";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.ok(root.childCount > 0);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_history_clear.js b/toolkit/components/places/tests/unit/test_history_clear.js
new file mode 100644
index 0000000000..7c121a5b84
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_clear.js
@@ -0,0 +1,146 @@
+/* -*- 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 mDBConn = DBConn();
+
+add_task(async function test_history_clear() {
+ await PlacesTestUtils.addVisits([
+ { uri: uri("http://typed.mozilla.org/"), transition: TRANSITION_TYPED },
+ { uri: uri("http://link.mozilla.org/"), transition: TRANSITION_LINK },
+ {
+ uri: uri("http://download.mozilla.org/"),
+ transition: TRANSITION_DOWNLOAD,
+ },
+ {
+ uri: uri("http://redir_temp.mozilla.org/"),
+ transition: TRANSITION_REDIRECT_TEMPORARY,
+ referrer: "http://link.mozilla.org/",
+ },
+ {
+ uri: uri("http://redir_perm.mozilla.org/"),
+ transition: TRANSITION_REDIRECT_PERMANENT,
+ referrer: "http://link.mozilla.org/",
+ },
+ ]);
+
+ // add a place: bookmark
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: `place:parent=${PlacesUtils.bookmarks.tagsGuid}`,
+ title: "shortcut",
+ });
+
+ // Add an expire never annotation
+ // Actually expire never annotations are removed as soon as a page is removed
+ // from the database, so this should act as a normal visit.
+ await PlacesUtils.history.update({
+ url: "http://download.mozilla.org/",
+ annotations: new Map([["never", "never"]]),
+ });
+
+ // Add a bookmark
+ // Bookmarked page should have history cleared and frecency to be recalculated
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://typed.mozilla.org/",
+ title: "bookmark",
+ });
+
+ await PlacesTestUtils.addVisits([
+ { uri: uri("http://typed.mozilla.org/"), transition: TRANSITION_BOOKMARK },
+ { uri: uri("http://frecency.mozilla.org/"), transition: TRANSITION_LINK },
+ ]);
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ // Clear history and wait for the history-cleared event notification.
+ let promiseClearHistory =
+ PlacesTestUtils.waitForNotification("history-cleared");
+ await PlacesUtils.history.clear();
+ await promiseClearHistory;
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ // Check that frecency for not cleared items (bookmarks) has been marked
+ // as to be recalculated.
+ let stmt = mDBConn.createStatement(
+ "SELECT h.id FROM moz_places h WHERE frecency <> 0 AND h.recalc_frecency = 0 "
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+
+ stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h WHERE h.recalc_frecency = 1
+ AND EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1`
+ );
+ Assert.ok(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that all visit_counts have been brought to 0
+ stmt = mDBConn.createStatement(
+ "SELECT id FROM moz_places WHERE visit_count <> 0 LIMIT 1"
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+
+ // Check that history tables are empty
+ stmt = mDBConn.createStatement(
+ "SELECT * FROM (SELECT id FROM moz_historyvisits LIMIT 1)"
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+
+ // Check that all moz_places entries except bookmarks and place: have been removed
+ stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h WHERE
+ url_hash NOT BETWEEN hash('place', 'prefix_lo') AND hash('place', 'prefix_hi')
+ AND NOT EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1`
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+
+ // Check that we only have favicons for retained places
+ stmt = mDBConn.createStatement(
+ `SELECT 1
+ FROM moz_pages_w_icons
+ LEFT JOIN moz_places h ON url_hash = page_url_hash AND url = page_url
+ WHERE h.id ISNULL`
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+ stmt = mDBConn.createStatement(
+ `SELECT 1
+ FROM moz_icons WHERE id NOT IN (
+ SELECT icon_id FROM moz_icons_to_pages
+ )`
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+
+ // Check that we only have annotations for retained places
+ stmt = mDBConn.createStatement(
+ `SELECT a.id FROM moz_annos a WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE id = a.place_id) LIMIT 1`
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+
+ // Check that we only have inputhistory for retained places
+ stmt = mDBConn.createStatement(
+ `SELECT i.place_id FROM moz_inputhistory i WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE id = i.place_id) LIMIT 1`
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+
+ // Check that place:uris have frecency 0
+ stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h
+ WHERE url_hash BETWEEN hash('place', 'prefix_lo')
+ AND hash('place', 'prefix_hi')
+ AND h.frecency <> 0 LIMIT 1`
+ );
+ Assert.ok(!stmt.executeStep());
+ stmt.finalize();
+});
diff --git a/toolkit/components/places/tests/unit/test_history_notifications.js b/toolkit/components/places/tests/unit/test_history_notifications.js
new file mode 100644
index 0000000000..339080b042
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_notifications.js
@@ -0,0 +1,50 @@
+const NS_PLACES_INIT_COMPLETE_TOPIC = "places-init-complete";
+let gLockedConn;
+
+add_task(async function setup() {
+ // Create a dummy places.sqlite and open an unshared connection on it
+ let db = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ db.append("places.sqlite");
+ gLockedConn = Services.storage.openUnsharedDatabase(db);
+ Assert.ok(db.exists(), "The database should have been created");
+
+ // We need an exclusive lock on the db
+ gLockedConn.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
+ // Exclusive locking is lazy applied, we need to make a write to activate it
+ gLockedConn.executeSimpleSQL("PRAGMA USER_VERSION = 1");
+});
+
+add_task(async function locked() {
+ // Try to create history service while the db is locked.
+ // It should be possible to create the service, but any method using the
+ // database will fail.
+ let resolved = false;
+ let promiseComplete = promiseTopicObserved(
+ NS_PLACES_INIT_COMPLETE_TOPIC
+ ).then(() => (resolved = true));
+ let history = Cc["@mozilla.org/browser/nav-history-service;1"].createInstance(
+ Ci.nsINavHistoryService
+ );
+ // The notification shouldn't happen until something tries to use the database.
+ await new Promise(resolve => do_timeout(100, resolve));
+ Assert.equal(
+ resolved,
+ false,
+ "The notification should not have been fired yet"
+ );
+ // This will initialize the database.
+ Assert.equal(history.databaseStatus, history.DATABASE_STATUS_LOCKED);
+ await promiseComplete;
+
+ // Close our connection and try to cleanup the file (could fail on Windows)
+ gLockedConn.close();
+ let db = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ db.append("places.sqlite");
+ if (db.exists()) {
+ try {
+ db.remove(false);
+ } catch (e) {
+ info("Unable to remove dummy places.sqlite");
+ }
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_history_observer.js b/toolkit/components/places/tests/unit/test_history_observer.js
new file mode 100644
index 0000000000..1a9323f890
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_observer.js
@@ -0,0 +1,186 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Registers a one-time places observer for 'page-visited',
+ * which resolves a promise on being called.
+ */
+function promiseVisitAdded(callback) {
+ return new Promise(resolve => {
+ async function listener(events) {
+ PlacesObservers.removeListener(["page-visited"], listener);
+ Assert.equal(events.length, 1, "Right number of visits notified");
+ Assert.equal(events[0].type, "page-visited");
+ await callback(events[0]);
+ resolve();
+ }
+ PlacesObservers.addListener(["page-visited"], listener);
+ });
+}
+
+/**
+ * Asynchronous task that adds a visit to the history database.
+ */
+async function task_add_visit(uri, timestamp, transition) {
+ uri = uri || NetUtil.newURI("http://firefox.com/");
+ timestamp = timestamp || Date.now() * 1000;
+ await PlacesTestUtils.addVisits({
+ uri,
+ transition: transition || TRANSITION_TYPED,
+ visitDate: timestamp,
+ });
+ return [uri, timestamp];
+}
+
+add_task(async function test_visitAdded() {
+ let promiseNotify = promiseVisitAdded(async function (visit) {
+ Assert.ok(visit.visitId > 0);
+ Assert.equal(visit.url, testuri.spec);
+ Assert.equal(visit.visitTime, testtime / 1000);
+ Assert.equal(visit.referringVisitId, 0);
+ Assert.equal(visit.transitionType, TRANSITION_TYPED);
+ let uri = NetUtil.newURI(visit.url);
+ await check_guid_for_uri(uri, visit.pageGuid);
+ Assert.ok(!visit.hidden);
+ Assert.equal(visit.visitCount, 1);
+ Assert.equal(visit.typedCount, 1);
+ });
+ let testuri = NetUtil.newURI("http://firefox.com/");
+ let testtime = Date.now() * 1000;
+ await task_add_visit(testuri, testtime);
+ await promiseNotify;
+});
+
+add_task(async function test_visitAdded() {
+ let promiseNotify = promiseVisitAdded(async function (visit) {
+ Assert.ok(visit.visitId > 0);
+ Assert.equal(visit.url, testuri.spec);
+ Assert.equal(visit.visitTime, testtime / 1000);
+ Assert.equal(visit.referringVisitId, 0);
+ Assert.equal(visit.transitionType, TRANSITION_FRAMED_LINK);
+ let uri = NetUtil.newURI(visit.url);
+ await check_guid_for_uri(uri, visit.pageGuid);
+ Assert.ok(visit.hidden);
+ Assert.equal(visit.visitCount, 1);
+ Assert.equal(visit.typedCount, 0);
+ });
+ let testuri = NetUtil.newURI("http://hidden.firefox.com/");
+ let testtime = Date.now() * 1000;
+ await task_add_visit(testuri, testtime, TRANSITION_FRAMED_LINK);
+ await promiseNotify;
+});
+
+add_task(async function test_multiple_onVisit() {
+ let testuri = NetUtil.newURI("http://self.firefox.com/");
+ let promiseNotifications = new Promise(resolve => {
+ async function listener(aEvents) {
+ Assert.equal(aEvents.length, 3, "Right number of visits notified");
+ for (let i = 0; i < aEvents.length; i++) {
+ Assert.equal(aEvents[i].type, "page-visited");
+ let visit = aEvents[i];
+ Assert.equal(testuri.spec, visit.url);
+ Assert.ok(visit.visitId > 0);
+ Assert.ok(visit.visitTime > 0);
+ Assert.ok(!visit.hidden);
+ let uri = NetUtil.newURI(visit.url);
+ await check_guid_for_uri(uri, visit.pageGuid);
+ switch (i) {
+ case 0:
+ Assert.equal(visit.referringVisitId, 0);
+ Assert.equal(visit.transitionType, TRANSITION_LINK);
+ Assert.equal(visit.visitCount, 1);
+ Assert.equal(visit.typedCount, 0);
+ break;
+ case 1:
+ Assert.ok(visit.referringVisitId > 0);
+ Assert.equal(visit.transitionType, TRANSITION_LINK);
+ Assert.equal(visit.visitCount, 2);
+ Assert.equal(visit.typedCount, 0);
+ break;
+ case 2:
+ Assert.equal(visit.referringVisitId, 0);
+ Assert.equal(visit.transitionType, TRANSITION_TYPED);
+ Assert.equal(visit.visitCount, 3);
+ Assert.equal(visit.typedCount, 1);
+
+ PlacesObservers.removeListener(["page-visited"], listener);
+ resolve();
+ break;
+ }
+ }
+ }
+ PlacesObservers.addListener(["page-visited"], listener);
+ });
+ await PlacesTestUtils.addVisits([
+ { uri: testuri, transition: TRANSITION_LINK },
+ { uri: testuri, referrer: testuri, transition: TRANSITION_LINK },
+ { uri: testuri, transition: TRANSITION_TYPED },
+ ]);
+ await promiseNotifications;
+});
+
+add_task(async function test_pageRemovedFromStore() {
+ let [testuri] = await task_add_visit();
+ let testguid = await PlacesTestUtils.getDatabaseValue("moz_places", "guid", {
+ url: testuri,
+ });
+
+ const promiseNotify = PlacesTestUtils.waitForNotification("page-removed");
+
+ await PlacesUtils.history.remove(testuri);
+
+ const events = await promiseNotify;
+ Assert.equal(events.length, 1, "Right number of page-removed notified");
+ Assert.equal(events[0].type, "page-removed");
+ Assert.ok(events[0].isRemovedFromStore);
+ Assert.equal(events[0].url, testuri.spec);
+ Assert.equal(events[0].pageGuid, testguid);
+ Assert.equal(events[0].reason, PlacesVisitRemoved.REASON_DELETED);
+});
+
+add_task(async function test_pageRemovedAllVisits() {
+ const promiseNotify = PlacesTestUtils.waitForNotification("page-removed");
+
+ let msecs24hrsAgo = Date.now() - 86400 * 1000;
+ let [testuri] = await task_add_visit(undefined, msecs24hrsAgo * 1000);
+ // Add a bookmark so the page is not removed.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test",
+ url: testuri,
+ });
+ let testguid = await PlacesTestUtils.getDatabaseValue("moz_places", "guid", {
+ url: testuri,
+ });
+ await PlacesUtils.history.remove(testuri);
+
+ const events = await promiseNotify;
+ Assert.equal(events.length, 1, "Right number of page-removed notified");
+ Assert.equal(events[0].type, "page-removed");
+ Assert.ok(!events[0].isRemovedFromStore);
+ Assert.equal(events[0].url, testuri.spec);
+ // Can't use do_check_guid_for_uri() here because the visit is already gone.
+ Assert.equal(events[0].pageGuid, testguid);
+ Assert.equal(events[0].reason, PlacesVisitRemoved.REASON_DELETED);
+ Assert.ok(!events[0].isPartialVisistsRemoval); // All visits have been removed.
+});
+
+add_task(async function test_pageTitleChanged() {
+ const [testuri] = await task_add_visit();
+ const title = "test-title";
+
+ const promiseNotify =
+ PlacesTestUtils.waitForNotification("page-title-changed");
+
+ await PlacesTestUtils.addVisits({
+ uri: testuri,
+ title,
+ });
+
+ const events = await promiseNotify;
+ Assert.equal(events.length, 1, "Right number of title changed notified");
+ Assert.equal(events[0].type, "page-title-changed");
+ Assert.equal(events[0].url, testuri.spec);
+ Assert.equal(events[0].title, title);
+ await check_guid_for_uri(testuri, events[0].pageGuid);
+});
diff --git a/toolkit/components/places/tests/unit/test_history_sidebar.js b/toolkit/components/places/tests/unit/test_history_sidebar.js
new file mode 100644
index 0000000000..868ef79f70
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_sidebar.js
@@ -0,0 +1,418 @@
+/* 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/. */
+
+let nowObj = new Date();
+
+/**
+ * Normalizes a Date to midnight.
+ *
+ * @param {Date} inputDate
+ * @return normalized Date
+ */
+function toMidnight(inputDate) {
+ let date = new Date(inputDate);
+ date.setHours(0);
+ date.setMinutes(0);
+ date.setSeconds(0);
+ date.setMilliseconds(0);
+ return date;
+}
+
+/**
+ * Adds a test URI visit to the database.
+ *
+ * @param aURI
+ * The URI to add a visit for.
+ * @param aTime
+ * Reference "now" time.
+ * @param aDayOffset
+ * number of days to add, pass a negative value to subtract them.
+ */
+async function addNormalizedVisit(aURI, aTime, aDayOffset) {
+ let dateObj = toMidnight(aTime);
+ // Days where DST changes should be taken into account.
+ let previousDateObj = new Date(dateObj.getTime() + aDayOffset * 86400000);
+ let DSTCorrection =
+ (dateObj.getTimezoneOffset() - previousDateObj.getTimezoneOffset()) *
+ 60 *
+ 1000;
+ // Substract aDayOffset
+ let PRTimeWithOffset = (previousDateObj.getTime() - DSTCorrection) * 1000;
+ info(
+ "Adding visit to " +
+ aURI.spec +
+ " at " +
+ PlacesUtils.toDate(PRTimeWithOffset)
+ );
+ await PlacesTestUtils.addVisits({
+ uri: aURI,
+ visitDate: PRTimeWithOffset,
+ });
+}
+
+function openRootForResultType(resultType) {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = resultType;
+ let query = PlacesUtils.history.getNewQuery();
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let root = result.root;
+ root.containerOpen = true;
+ return result;
+}
+
+function daysForMonthsAgo(months) {
+ let oldTime = toMidnight(new Date());
+ // Set day before month, otherwise we could try to calculate 30 February, or
+ // other nonexistent days.
+ oldTime.setDate(1);
+ oldTime.setMonth(nowObj.getMonth() - months);
+ // Stay larger for eventual timezone issues, add 2 days.
+ return parseInt((nowObj - oldTime) / (1000 * 60 * 60 * 24)) + 2;
+}
+
+// This test relies on en-US locale
+// Offset is number of days
+let containers = [
+ { label: "Today", offset: 0, visible: true },
+ { label: "Yesterday", offset: -1, visible: true },
+ { label: "Last 7 days", offset: -2, visible: true },
+ { label: "This month", offset: -8, visible: nowObj.getDate() > 8 },
+ { label: "", offset: -daysForMonthsAgo(0), visible: true },
+ { label: "", offset: -daysForMonthsAgo(1), visible: true },
+ { label: "", offset: -daysForMonthsAgo(2), visible: true },
+ { label: "", offset: -daysForMonthsAgo(3), visible: true },
+ { label: "", offset: -daysForMonthsAgo(4), visible: true },
+ { label: "Older than 6 months", offset: -daysForMonthsAgo(5), visible: true },
+];
+
+let visibleContainers = containers.filter(container => container.visible);
+
+/**
+ * Asynchronous task that fills history and checks containers' labels.
+ */
+add_task(async function task_fill_history() {
+ info("*** TEST Fill History");
+ // We can't use "now" because our hardcoded offsets would be invalid for some
+ // date. So we hardcode a date.
+ for (let i = 0; i < containers.length; i++) {
+ let container = containers[i];
+ let testURI = uri("http://mirror" + i + ".mozilla.com/b");
+ await addNormalizedVisit(testURI, nowObj, container.offset);
+ testURI = uri("http://mirror" + i + ".mozilla.com/a");
+ await addNormalizedVisit(testURI, nowObj, container.offset);
+ testURI = uri("http://mirror" + i + ".google.com/b");
+ await addNormalizedVisit(testURI, nowObj, container.offset);
+ testURI = uri("http://mirror" + i + ".google.com/a");
+ await addNormalizedVisit(testURI, nowObj, container.offset);
+ // Bug 485703 - Hide date containers not containing additional entries
+ // compared to previous ones.
+ // Check after every new container is added.
+ check_visit(container.offset);
+ }
+
+ let root = openRootForResultType(
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ ).root;
+ let cc = root.childCount;
+ info("Found containers:");
+ let previousLabels = [];
+ for (let i = 0; i < cc; i++) {
+ let container = visibleContainers[i];
+ let node = root.getChild(i);
+ info(node.title);
+ if (container.label) {
+ Assert.equal(node.title, container.label);
+ }
+ // Check labels are not repeated.
+ Assert.ok(!previousLabels.includes(node.title));
+ previousLabels.push(node.title);
+ }
+ Assert.equal(cc, visibleContainers.length);
+ root.containerOpen = false;
+});
+
+/**
+ * Bug 485703 - Hide date containers not containing additional entries compared
+ * to previous ones.
+ */
+function check_visit(aOffset) {
+ let root = openRootForResultType(
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ ).root;
+
+ let unexpected = [];
+ switch (aOffset) {
+ case 0:
+ unexpected = ["Yesterday", "Last 7 days", "This month"];
+ break;
+ case -1:
+ unexpected = ["Last 7 days", "This month"];
+ break;
+ case -2:
+ unexpected = ["This month"];
+ break;
+ default:
+ // Other containers are tested later.
+ }
+
+ info("Found containers:");
+ let cc = root.childCount;
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ info(node.title);
+ Assert.ok(!unexpected.includes(node.title));
+ }
+ root.containerOpen = false;
+}
+
+/**
+ * Queries history grouped by date and site, checking containers' labels and
+ * children.
+ */
+add_task(async function test_RESULTS_AS_DATE_SITE_QUERY() {
+ info("*** TEST RESULTS_AS_DATE_SITE_QUERY");
+ let result = openRootForResultType(
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ );
+ let root = result.root;
+
+ // Check one of the days
+ let dayNode = PlacesUtils.asContainer(root.getChild(0));
+ dayNode.containerOpen = true;
+ Assert.equal(dayNode.childCount, 2);
+
+ // Items should be sorted by host
+ let site1 = PlacesUtils.asContainer(dayNode.getChild(0));
+ Assert.equal(site1.title, "mirror0.google.com");
+
+ let site2 = PlacesUtils.asContainer(dayNode.getChild(1));
+ Assert.equal(site2.title, "mirror0.mozilla.com");
+
+ site1.containerOpen = true;
+ Assert.equal(site1.childCount, 2);
+
+ // Inside of host sites are sorted by title
+ let site1visit = site1.getChild(0);
+ Assert.equal(site1visit.uri, "http://mirror0.google.com/a");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING;
+
+ // Check one of the days
+ dayNode = PlacesUtils.asContainer(root.getChild(0));
+ dayNode.containerOpen = true;
+ Assert.equal(dayNode.childCount, 2);
+
+ // Hosts are still sorted by title
+ site1 = PlacesUtils.asContainer(dayNode.getChild(0));
+ Assert.equal(site1.title, "mirror0.google.com");
+
+ site2 = PlacesUtils.asContainer(dayNode.getChild(1));
+ Assert.equal(site2.title, "mirror0.mozilla.com");
+
+ site1.containerOpen = true;
+ Assert.equal(site1.childCount, 2);
+
+ // But URLs are now sorted by title descending
+ site1visit = site1.getChild(0);
+ Assert.equal(site1visit.uri, "http://mirror0.google.com/b");
+
+ site1.containerOpen = false;
+ dayNode.containerOpen = false;
+ root.containerOpen = false;
+});
+
+/**
+ * Queries history grouped by date, checking containers' labels and children.
+ */
+add_task(async function test_RESULTS_AS_DATE_QUERY() {
+ info("*** TEST RESULTS_AS_DATE_QUERY");
+ let result = openRootForResultType(
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY
+ );
+ let root = result.root;
+ let cc = root.childCount;
+ Assert.equal(cc, visibleContainers.length);
+ info("Found containers:");
+ for (let i = 0; i < cc; i++) {
+ let container = visibleContainers[i];
+ let node = root.getChild(i);
+ info(node.title);
+ if (container.label) {
+ Assert.equal(node.title, container.label);
+ }
+ }
+
+ // Check one of the days
+ let dayNode = PlacesUtils.asContainer(root.getChild(0));
+ dayNode.containerOpen = true;
+ Assert.equal(dayNode.childCount, 4);
+
+ // Items should be sorted by title
+ let visit1 = dayNode.getChild(0);
+ Assert.equal(visit1.uri, "http://mirror0.google.com/a");
+
+ let visit2 = dayNode.getChild(3);
+ Assert.equal(visit2.uri, "http://mirror0.mozilla.com/b");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_DESCENDING;
+
+ // Check one of the days
+ dayNode = PlacesUtils.asContainer(root.getChild(0));
+ dayNode.containerOpen = true;
+ Assert.equal(dayNode.childCount, 4);
+
+ // But URLs are now sorted by title descending
+ visit1 = dayNode.getChild(0);
+ Assert.equal(visit1.uri, "http://mirror0.mozilla.com/b");
+
+ visit2 = dayNode.getChild(3);
+ Assert.equal(visit2.uri, "http://mirror0.google.com/a");
+
+ dayNode.containerOpen = false;
+ root.containerOpen = false;
+});
+
+/**
+ * Queries history grouped by site, checking containers' labels and children.
+ */
+add_task(async function test_RESULTS_AS_SITE_QUERY() {
+ info("*** TEST RESULTS_AS_SITE_QUERY");
+ // add a bookmark with a domain not in the set of visits in the db
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://foobar",
+ title: "",
+ });
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_SITE_QUERY;
+ options.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ let query = PlacesUtils.history.getNewQuery();
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, containers.length * 2);
+
+ // Expected results:
+ // "mirror0.google.com",
+ // "mirror0.mozilla.com",
+ // "mirror1.google.com",
+ // "mirror1.mozilla.com",
+ // "mirror2.google.com",
+ // "mirror2.mozilla.com",
+ // "mirror3.google.com", <== We check for this site (index 6)
+ // "mirror3.mozilla.com",
+ // "mirror4.google.com",
+ // "mirror4.mozilla.com",
+ // "mirror5.google.com",
+ // "mirror5.mozilla.com",
+ // ...
+
+ // Items should be sorted by host
+ let siteNode = PlacesUtils.asContainer(root.getChild(6));
+ Assert.equal(siteNode.title, "mirror3.google.com");
+
+ siteNode.containerOpen = true;
+ Assert.equal(siteNode.childCount, 2);
+
+ // Inside of host sites are sorted by title
+ let visitNode = siteNode.getChild(0);
+ Assert.equal(visitNode.uri, "http://mirror3.google.com/a");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = options.SORT_BY_TITLE_DESCENDING;
+ siteNode = PlacesUtils.asContainer(root.getChild(6));
+ Assert.equal(siteNode.title, "mirror3.google.com");
+
+ siteNode.containerOpen = true;
+ Assert.equal(siteNode.childCount, 2);
+
+ // But URLs are now sorted by title descending
+ let visit = siteNode.getChild(0);
+ Assert.equal(visit.uri, "http://mirror3.google.com/b");
+
+ siteNode.containerOpen = false;
+ root.containerOpen = false;
+
+ // Cleanup.
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+});
+
+/**
+ * Checks that queries grouped by date do liveupdate correctly.
+ */
+async function test_date_liveupdate(aResultType) {
+ let midnight = toMidnight(nowObj);
+
+ // TEST 1. Test that the query correctly updates when it is root.
+ let root = openRootForResultType(aResultType).root;
+ Assert.equal(root.childCount, visibleContainers.length);
+
+ // Remove "Today".
+ await PlacesUtils.history.removeByFilter({
+ beginDate: new Date(midnight.getTime()),
+ endDate: new Date(Date.now()),
+ });
+ Assert.equal(root.childCount, visibleContainers.length - 1);
+
+ // Open "Last 7 days" container, this way we will have a container accepting
+ // the new visit, but we should still add back "Today" container.
+ let last7Days = PlacesUtils.asContainer(root.getChild(1));
+ last7Days.containerOpen = true;
+
+ // Add a visit for "Today". This should add back the missing "Today"
+ // container.
+ await addNormalizedVisit(uri("http://www.mozilla.org/"), nowObj, 0);
+ Assert.equal(root.childCount, visibleContainers.length);
+
+ last7Days.containerOpen = false;
+ root.containerOpen = false;
+
+ // TEST 2. Test that the query correctly updates even if it is not root.
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "place:type=" + aResultType,
+ title: "",
+ });
+
+ // Query toolbar and open our query container, then check again liveupdate.
+ root = PlacesUtils.getFolderContents(PlacesUtils.bookmarks.toolbarGuid).root;
+ Assert.equal(root.childCount, 1);
+ let dateContainer = PlacesUtils.asContainer(root.getChild(0));
+ dateContainer.containerOpen = true;
+
+ Assert.equal(dateContainer.childCount, visibleContainers.length);
+ // Remove "Today".
+ await PlacesUtils.history.removeByFilter({
+ beginDate: new Date(midnight.getTime()),
+ endDate: new Date(Date.now()),
+ });
+ Assert.equal(dateContainer.childCount, visibleContainers.length - 1);
+ // Add a visit for "Today".
+ await addNormalizedVisit(uri("http://www.mozilla.org/"), nowObj, 0);
+ Assert.equal(dateContainer.childCount, visibleContainers.length);
+
+ dateContainer.containerOpen = false;
+ root.containerOpen = false;
+
+ // Cleanup.
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+}
+
+add_task(async function test_history_sidebar() {
+ await test_date_liveupdate(
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY
+ );
+ await test_date_liveupdate(
+ Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_QUERY
+ );
+
+ // The remaining views are
+ // RESULTS_AS_URI + SORT_BY_VISITCOUNT_DESCENDING
+ // -> test_399266.js
+ // RESULTS_AS_URI + SORT_BY_DATE_DESCENDING
+ // -> test_385397.js
+});
diff --git a/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
new file mode 100644
index 0000000000..167b8786e4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -0,0 +1,326 @@
+async function importFromFixture(fixture, replace) {
+ let cwd = do_get_cwd().path;
+ let path = PathUtils.join(cwd, fixture);
+
+ info(`Importing from ${path}`);
+ await BookmarkJSONUtils.importFromFile(path, { replace });
+ await PlacesTestUtils.promiseAsyncUpdates();
+}
+
+async function treeEquals(guid, expected, message) {
+ let root = await PlacesUtils.promiseBookmarksTree(guid);
+ let bookmarks = (function nodeToEntry(node) {
+ let entry = { guid: node.guid, index: node.index };
+ if (node.children) {
+ entry.children = node.children.map(nodeToEntry);
+ }
+ return entry;
+ })(root);
+
+ info(`Checking if ${guid} tree matches ${JSON.stringify(expected)}`);
+ info(`Got bookmarks tree for ${guid}: ${JSON.stringify(bookmarks)}`);
+
+ deepEqual(bookmarks, expected, message);
+}
+
+add_task(async function test_restore_mobile_bookmarks_root() {
+ await importFromFixture(
+ "mobile_bookmarks_root_import.json",
+ /* replace */ true
+ );
+
+ await treeEquals(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [{ guid: "X6lUyOspVYwi", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ },
+ ],
+ },
+ "Should restore mobile bookmarks from root"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_mobile_bookmarks_root() {
+ await importFromFixture(
+ "mobile_bookmarks_root_import.json",
+ /* replace */ false
+ );
+ await importFromFixture(
+ "mobile_bookmarks_root_merge.json",
+ /* replace */ false
+ );
+
+ await treeEquals(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "Utodo9b0oVws", index: 1 },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ children: [
+ // The first two are in ..._import.json, the second two are in
+ // ..._merge.json
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ { guid: "a17yW6-nTxEJ", index: 2 },
+ { guid: "xV10h9Wi3FBM", index: 3 },
+ ],
+ },
+ ],
+ },
+ "Should merge bookmarks root contents"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_restore_mobile_bookmarks_folder() {
+ // This tests importing a mobile bookmarks folder with the annotation,
+ // and the old, random guid.
+ await importFromFixture(
+ "mobile_bookmarks_folder_import.json",
+ /* replace */ true
+ );
+
+ await treeEquals(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "XF4yRP6bTuil", index: 1 },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "buy7711R3ZgE", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ },
+ ],
+ },
+ "Should restore mobile bookmark folder contents into mobile root"
+ );
+
+ let queryById = await PlacesUtils.bookmarks.fetch("XF4yRP6bTuil");
+ equal(
+ queryById.url.href,
+ `place:parent=${PlacesUtils.bookmarks.mobileGuid}`,
+ "Should rewrite mobile query to point to root GUID"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_mobile_bookmarks_folder() {
+ await importFromFixture(
+ "mobile_bookmarks_folder_import.json",
+ /* replace */ false
+ );
+ await importFromFixture(
+ "mobile_bookmarks_folder_merge.json",
+ /* replace */ false
+ );
+
+ await treeEquals(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "XF4yRP6bTuil", index: 1 },
+ { guid: "Utodo9b0oVws", index: 2 },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "buy7711R3ZgE", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "KIa9iKZab2Z5", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ { guid: "a17yW6-nTxEJ", index: 2 },
+ { guid: "xV10h9Wi3FBM", index: 3 },
+ ],
+ },
+ ],
+ },
+ "Should merge bookmarks folder contents into mobile root"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_restore_multiple_bookmarks_folders() {
+ await importFromFixture(
+ "mobile_bookmarks_multiple_folders.json",
+ /* replace */ true
+ );
+
+ await treeEquals(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "buy7711R3ZgE", index: 0 },
+ { guid: "F_LBgd1fS_uQ", index: 1 },
+ { guid: "oIpmQXMWsXvY", index: 2 },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "Utodo9b0oVws", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ children: [
+ { guid: "a17yW6-nTxEJ", index: 0 },
+ { guid: "sSZ86WT9WbN3", index: 1 },
+ ],
+ },
+ ],
+ },
+ "Should restore multiple bookmarks folder contents into root"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_import_multiple_bookmarks_folders() {
+ await importFromFixture(
+ "mobile_bookmarks_root_import.json",
+ /* replace */ false
+ );
+ await importFromFixture(
+ "mobile_bookmarks_multiple_folders.json",
+ /* replace */ false
+ );
+
+ await treeEquals(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "X6lUyOspVYwi", index: 0 },
+ { guid: "buy7711R3ZgE", index: 1 },
+ { guid: "F_LBgd1fS_uQ", index: 2 },
+ { guid: "oIpmQXMWsXvY", index: 3 },
+ ],
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ children: [{ guid: "Utodo9b0oVws", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ children: [{ guid: "xV10h9Wi3FBM", index: 0 }],
+ },
+ {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ { guid: "a17yW6-nTxEJ", index: 2 },
+ { guid: "sSZ86WT9WbN3", index: 3 },
+ ],
+ },
+ ],
+ },
+ "Should merge multiple mobile folders into root"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_isPageInDB.js b/toolkit/components/places/tests/unit/test_isPageInDB.js
new file mode 100644
index 0000000000..2eda125994
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isPageInDB.js
@@ -0,0 +1,10 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+add_task(async function test_execute() {
+ var good_uri = uri("http://mozilla.com");
+ var bad_uri = uri("http://google.com");
+ await PlacesTestUtils.addVisits({ uri: good_uri });
+ Assert.ok(await PlacesTestUtils.isPageInDB(good_uri));
+ Assert.equal(false, await PlacesTestUtils.isPageInDB(bad_uri));
+});
diff --git a/toolkit/components/places/tests/unit/test_isURIVisited.js b/toolkit/components/places/tests/unit/test_isURIVisited.js
new file mode 100644
index 0000000000..ac40e5fba6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isURIVisited.js
@@ -0,0 +1,73 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests functionality of the isURIVisited API.
+
+const SCHEMES = {
+ "http://": true,
+ "https://": true,
+ "ftp://": true,
+ "file:///": true,
+ "about:": false,
+ // nsIIOService.newURI() can throw if e.g. the app knows about imap://
+ // but the account is not set up and so the URL is invalid for it.
+ // "imap://": false,
+ "news://": false,
+ "mailbox:": false,
+ "cached-favicon:http://": false,
+ "view-source:http://": false,
+ "chrome://browser/content/browser.xhtml?": false,
+ "resource://": false,
+ "data:,": false,
+ "javascript:": false,
+};
+
+add_task(async function test_isURIVisited() {
+ let history = Cc["@mozilla.org/browser/history;1"].getService(
+ Ci.mozIAsyncHistory
+ );
+
+ function visitsPromise(uri) {
+ return new Promise(resolve => {
+ history.isURIVisited(uri, (receivedURI, visited) => {
+ resolve([receivedURI, visited]);
+ });
+ });
+ }
+
+ for (let scheme in SCHEMES) {
+ info("Testing scheme " + scheme);
+ for (let t in PlacesUtils.history.TRANSITIONS) {
+ if (t == "EMBED") {
+ continue;
+ }
+ info("With transition " + t);
+ let aTransition = PlacesUtils.history.TRANSITIONS[t];
+
+ let aURI = Services.io.newURI(scheme + "mozilla.org/");
+
+ let [receivedURI1, visited1] = await visitsPromise(aURI);
+ Assert.ok(aURI.equals(receivedURI1));
+ Assert.ok(!visited1);
+
+ if (PlacesUtils.history.canAddURI(aURI)) {
+ await PlacesTestUtils.addVisits([
+ {
+ uri: aURI,
+ transition: aTransition,
+ },
+ ]);
+ info("Added visit for " + aURI.spec);
+ }
+
+ let [receivedURI2, visited2] = await visitsPromise(aURI);
+ Assert.ok(aURI.equals(receivedURI2));
+ Assert.equal(SCHEMES[scheme], visited2);
+
+ await PlacesUtils.history.clear();
+ let [receivedURI3, visited3] = await visitsPromise(aURI);
+ Assert.ok(aURI.equals(receivedURI3));
+ Assert.ok(!visited3);
+ }
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_isvisited.js b/toolkit/components/places/tests/unit/test_isvisited.js
new file mode 100644
index 0000000000..7271ef7903
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isvisited.js
@@ -0,0 +1,69 @@
+/* -*- 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 test_execute() {
+ var referrer = uri("about:blank");
+
+ // add a http:// uri
+ var uri1 = uri("http://mozilla.com");
+ await PlacesTestUtils.addVisits({ uri: uri1, referrer });
+ await check_guid_for_uri(uri1);
+ Assert.ok(await PlacesUtils.history.hasVisits(uri1));
+
+ // add a https:// uri
+ var uri2 = uri("https://etrade.com");
+ await PlacesTestUtils.addVisits({ uri: uri2, referrer });
+ await check_guid_for_uri(uri2);
+ Assert.ok(await PlacesUtils.history.hasVisits(uri2));
+
+ // add a ftp:// uri
+ var uri3 = uri("ftp://ftp.mozilla.org");
+ await PlacesTestUtils.addVisits({ uri: uri3, referrer });
+ await check_guid_for_uri(uri3);
+ Assert.ok(await PlacesUtils.history.hasVisits(uri3));
+
+ // check if a nonexistent uri is visited
+ var uri4 = uri("http://foobarcheese.com");
+ Assert.equal(false, await PlacesUtils.history.hasVisits(uri4));
+
+ // check that certain schemes never show up as visited
+ // even if we attempt to add them to history
+ // see CanAddURI() in nsNavHistory.cpp
+ const URLS = [
+ "about:config",
+ "imap://cyrus.andrew.cmu.edu/archive.imap",
+ "news://new.mozilla.org/mozilla.dev.apps.firefox",
+ "mailbox:Inbox",
+ "cached-favicon:http://mozilla.org/made-up-favicon",
+ "view-source:http://mozilla.org",
+ "chrome://browser/content/browser.xhtml",
+ "resource://gre-resources/hiddenWindow.html",
+ "data:,Hello%2C%20World!",
+ "javascript:alert('hello wolrd!');",
+ "http://localhost/" + "a".repeat(1984),
+ ];
+ for (let currentURL of URLS) {
+ try {
+ var cantAddUri = uri(currentURL);
+ } catch (e) {
+ // nsIIOService.newURI() can throw if e.g. our app knows about imap://
+ // but the account is not set up and so the URL is invalid for us.
+ // Note this in the log but ignore as it's not the subject of this test.
+ info("Could not construct URI for '" + currentURL + "'; ignoring");
+ }
+ if (cantAddUri) {
+ PlacesTestUtils.addVisits({ uri: cantAddUri, referrer }).then(
+ () => {
+ do_throw("Should not have added history for invalid URI.");
+ },
+ error => {
+ Assert.ok(error.message.includes("No items were added to history"));
+ }
+ );
+ Assert.equal(false, await PlacesUtils.history.hasVisits(cantAddUri));
+ }
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_keywords.js b/toolkit/components/places/tests/unit/test_keywords.js
new file mode 100644
index 0000000000..8ee3ecafe2
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_keywords.js
@@ -0,0 +1,733 @@
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ Preferences: "resource://gre/modules/Preferences.sys.mjs",
+});
+
+async function check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) {
+ // Check case-insensitivity.
+ aKeyword = aKeyword.toUpperCase();
+
+ let entry = await PlacesUtils.keywords.fetch(aKeyword);
+
+ Assert.deepEqual(
+ entry,
+ await PlacesUtils.keywords.fetch({ keyword: aKeyword })
+ );
+
+ if (aExpectExists) {
+ Assert.ok(!!entry, "A keyword should exist");
+ Assert.equal(entry.url.href, aHref);
+ Assert.equal(entry.postData, aPostData);
+ Assert.deepEqual(
+ entry,
+ await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })
+ );
+ let entries = [];
+ await PlacesUtils.keywords.fetch({ url: aHref }, e => entries.push(e));
+ Assert.ok(
+ entries.some(
+ e => e.url.href == aHref && e.keyword == aKeyword.toLowerCase()
+ )
+ );
+ } else {
+ Assert.ok(
+ !entry || entry.url.href != aHref,
+ "The given keyword entry should not exist"
+ );
+ if (aHref) {
+ Assert.equal(
+ null,
+ await PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref })
+ );
+ } else {
+ Assert.equal(
+ null,
+ await PlacesUtils.keywords.fetch({ keyword: aKeyword })
+ );
+ }
+ }
+}
+
+/**
+ * Polls the keywords cache waiting for the given keyword entry.
+ */
+async function promiseKeyword(keyword, expectedHref) {
+ let href = null;
+ do {
+ await new Promise(resolve => do_timeout(100, resolve));
+ let entry = await PlacesUtils.keywords.fetch(keyword);
+ if (entry) {
+ href = entry.url.href;
+ }
+ } while (href != expectedHref);
+}
+
+async function check_no_orphans() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.executeCached(
+ `SELECT id FROM moz_keywords k
+ WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id)
+ `
+ );
+ Assert.equal(rows.length, 0);
+}
+
+function expectBookmarkNotifications() {
+ const observer = {
+ notifications: [],
+ _start() {
+ this._handle = this._handle.bind(this);
+ PlacesUtils.observers.addListener(
+ ["bookmark-keyword-changed"],
+ this._handle
+ );
+ },
+ _handle(events) {
+ for (const event of events) {
+ this.notifications.push({
+ type: event.type,
+ id: event.id,
+ itemType: event.itemType,
+ url: event.url,
+ guid: event.guid,
+ parentGuid: event.parentGuid,
+ keyword: event.keyword,
+ lastModified: new Date(event.lastModified),
+ source: event.source,
+ isTagging: event.isTagging,
+ });
+ }
+ },
+ check(expected) {
+ PlacesUtils.observers.removeListener(
+ ["bookmark-keyword-changed"],
+ this._handle
+ );
+ Assert.deepEqual(this.notifications, expected);
+ },
+ };
+ observer._start();
+ return observer;
+}
+
+add_task(async function test_invalid_input() {
+ Assert.throws(() => PlacesUtils.keywords.fetch(null), /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch(5), /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch(undefined), /Invalid keyword/);
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ keyword: null }),
+ /Invalid keyword/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ keyword: {} }),
+ /Invalid keyword/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ keyword: 5 }),
+ /Invalid keyword/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({}),
+ /At least keyword or url must be provided/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ keyword: "test" }, "test"),
+ /onResult callback must be a valid function/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ url: "test" }),
+ /is not a valid URL/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ url: {} }),
+ /is not a valid URL/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ url: null }),
+ /is not a valid URL/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.fetch({ url: "" }),
+ /is not a valid URL/
+ );
+
+ Assert.throws(
+ () => PlacesUtils.keywords.insert(null),
+ /Input should be a valid object/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert("test"),
+ /Input should be a valid object/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert(undefined),
+ /Input should be a valid object/
+ );
+ Assert.throws(() => PlacesUtils.keywords.insert({}), /Invalid keyword/);
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: null }),
+ /Invalid keyword/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: 5 }),
+ /Invalid keyword/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "" }),
+ /Invalid keyword/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "test", postData: 5 }),
+ /Invalid POST data/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "test", postData: {} }),
+ /Invalid POST data/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "test" }),
+ /is not a valid URL/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "test", url: 5 }),
+ /is not a valid URL/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "test", url: "" }),
+ /is not a valid URL/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "test", url: null }),
+ /is not a valid URL/
+ );
+ Assert.throws(
+ () => PlacesUtils.keywords.insert({ keyword: "test", url: "mozilla" }),
+ /is not a valid URL/
+ );
+
+ Assert.throws(() => PlacesUtils.keywords.remove(null), /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.remove(""), /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.remove(5), /Invalid keyword/);
+});
+
+add_task(async function test_addKeyword() {
+ await check_keyword(false, "http://example.com/", "keyword");
+ let fc = await foreign_count("http://example.com/");
+ let observer = expectBookmarkNotifications();
+
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ });
+ observer.check([]);
+
+ await check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.remove("keyword");
+ observer.check([]);
+
+ await check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc); // -1 keyword
+
+ // Check using URL.
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: new URL("http://example.com/"),
+ });
+ await check_keyword(true, "http://example.com/", "keyword");
+ await PlacesUtils.keywords.remove("keyword");
+ await check_keyword(false, "http://example.com/", "keyword");
+
+ await check_no_orphans();
+});
+
+add_task(async function test_addBookmarkAndKeyword() {
+ let timerPrecision = Preferences.get("privacy.reduceTimerPrecision");
+ Preferences.set("privacy.reduceTimerPrecision", false);
+
+ registerCleanupFunction(function () {
+ Preferences.set("privacy.reduceTimerPrecision", timerPrecision);
+ });
+
+ await check_keyword(false, "http://example.com/", "keyword");
+ let fc = await foreign_count("http://example.com/");
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ let observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ });
+
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark.guid),
+ itemType: bookmark.type,
+ url: bookmark.url,
+ guid: bookmark.guid,
+ parentGuid: bookmark.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ await check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.remove("keyword");
+
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark.guid),
+ itemType: bookmark.type,
+ url: bookmark.url,
+ guid: bookmark.guid,
+ parentGuid: bookmark.parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmark.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ await check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 1); // -1 keyword
+
+ // Add again the keyword, then remove the bookmark.
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ });
+
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+ // the notification is synchronous but the removal process is async.
+ // Unfortunately there's nothing explicit we can wait for.
+ // eslint-disable-next-line no-empty
+ while (await foreign_count("http://example.com/")) {}
+ // We don't get any itemChanged notification since the bookmark has been
+ // removed already.
+ observer.check([]);
+
+ await check_keyword(false, "http://example.com/", "keyword");
+
+ await check_no_orphans();
+});
+
+add_task(async function test_addKeywordToURIHavingKeyword() {
+ await check_keyword(false, "http://example.com/", "keyword");
+ let fc = await foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ });
+ observer.check([]);
+
+ await check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword
+
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword2",
+ url: "http://example.com/",
+ postData: "test=1",
+ });
+
+ await check_keyword(true, "http://example.com/", "keyword");
+ await check_keyword(true, "http://example.com/", "keyword2", "test=1");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 keyword
+ let entries = [];
+ let entry = await PlacesUtils.keywords.fetch(
+ { url: "http://example.com/" },
+ e => entries.push(e)
+ );
+ Assert.equal(entries.length, 2);
+ Assert.deepEqual(entries[0], entry);
+
+ // Now remove the keywords.
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.remove("keyword");
+ await PlacesUtils.keywords.remove("keyword2");
+ observer.check([]);
+
+ await check_keyword(false, "http://example.com/", "keyword");
+ await check_keyword(false, "http://example.com/", "keyword2");
+ Assert.equal(await foreign_count("http://example.com/"), fc); // -1 keyword
+
+ await check_no_orphans();
+});
+
+add_task(async function test_addBookmarkToURIHavingKeyword() {
+ await check_keyword(false, "http://example.com/", "keyword");
+ let fc = await foreign_count("http://example.com/");
+ let observer = expectBookmarkNotifications();
+
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ });
+ observer.check([]);
+
+ await check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 keyword
+
+ observer = expectBookmarkNotifications();
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark
+ observer.check([]);
+
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+ // the notification is synchronous but the removal process is async.
+ // Unfortunately there's nothing explicit we can wait for.
+ // eslint-disable-next-line no-empty
+ while (await foreign_count("http://example.com/")) {}
+ // We don't get any itemChanged notification since the bookmark has been
+ // removed already.
+ observer.check([]);
+
+ await check_keyword(false, "http://example.com/", "keyword");
+
+ await check_no_orphans();
+});
+
+add_task(async function test_sameKeywordDifferentURL() {
+ let fc1 = await foreign_count("http://example1.com/");
+ let bookmark1 = await PlacesUtils.bookmarks.insert({
+ url: "http://example1.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ let fc2 = await foreign_count("http://example2.com/");
+ let bookmark2 = await PlacesUtils.bookmarks.insert({
+ url: "http://example2.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example1.com/",
+ });
+
+ await check_keyword(true, "http://example1.com/", "keyword");
+ Assert.equal(await foreign_count("http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword
+ await check_keyword(false, "http://example2.com/", "keyword");
+ Assert.equal(await foreign_count("http://example2.com/"), fc2 + 1); // +1 bookmark
+
+ // Assign the same keyword to another url.
+ let observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example2.com/",
+ });
+
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark1.guid),
+ itemType: bookmark1.type,
+ url: bookmark1.url,
+ guid: bookmark1.guid,
+ parentGuid: bookmark1.parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmark1.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
+ itemType: bookmark2.type,
+ url: bookmark2.url,
+ guid: bookmark2.guid,
+ parentGuid: bookmark2.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark2.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ await check_keyword(false, "http://example1.com/", "keyword");
+ Assert.equal(await foreign_count("http://example1.com/"), fc1 + 1); // -1 keyword
+ await check_keyword(true, "http://example2.com/", "keyword");
+ Assert.equal(await foreign_count("http://example2.com/"), fc2 + 2); // +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.remove("keyword");
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
+ itemType: bookmark2.type,
+ url: bookmark2.url,
+ guid: bookmark2.guid,
+ parentGuid: bookmark2.parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmark2.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ await check_keyword(false, "http://example1.com/", "keyword");
+ await check_keyword(false, "http://example2.com/", "keyword");
+ Assert.equal(await foreign_count("http://example1.com/"), fc1 + 1);
+ Assert.equal(await foreign_count("http://example2.com/"), fc2 + 1); // -1 keyword
+
+ await PlacesUtils.bookmarks.remove(bookmark1);
+ await PlacesUtils.bookmarks.remove(bookmark2);
+ Assert.equal(await foreign_count("http://example1.com/"), fc1); // -1 bookmark
+ // eslint-disable-next-line no-empty
+ while (await foreign_count("http://example2.com/")) {} // -1 keyword
+
+ await check_no_orphans();
+});
+
+add_task(async function test_sameURIDifferentKeyword() {
+ let fc = await foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ });
+
+ await check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 keyword
+
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark.guid),
+ itemType: bookmark.type,
+ url: bookmark.url,
+ guid: bookmark.guid,
+ parentGuid: bookmark.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword2",
+ url: "http://example.com/",
+ });
+ await check_keyword(false, "http://example.com/", "keyword");
+ await check_keyword(true, "http://example.com/", "keyword2");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 2); // -1 keyword +1 keyword
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark.guid),
+ itemType: bookmark.type,
+ url: bookmark.url,
+ guid: bookmark.guid,
+ parentGuid: bookmark.parentGuid,
+ keyword: "keyword2",
+ lastModified: new Date(bookmark.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ // Now remove the bookmark.
+ await PlacesUtils.bookmarks.remove(bookmark);
+ // eslint-disable-next-line no-empty
+ while (await foreign_count("http://example.com/")) {}
+ await check_keyword(false, "http://example.com/", "keyword");
+ await check_keyword(false, "http://example.com/", "keyword2");
+
+ await check_no_orphans();
+});
+
+add_task(async function test_deleteKeywordMultipleBookmarks() {
+ let fc = await foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ let bookmark1 = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ let bookmark2 = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ });
+
+ await check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 3); // +2 bookmark +1 keyword
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
+ itemType: bookmark2.type,
+ url: bookmark2.url,
+ guid: bookmark2.guid,
+ parentGuid: bookmark2.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark2.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark1.guid),
+ itemType: bookmark1.type,
+ url: bookmark1.url,
+ guid: bookmark1.guid,
+ parentGuid: bookmark1.parentGuid,
+ keyword: "keyword",
+ lastModified: new Date(bookmark1.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ observer = expectBookmarkNotifications();
+ await PlacesUtils.keywords.remove("keyword");
+ await check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal(await foreign_count("http://example.com/"), fc + 2); // -1 keyword
+ observer.check([
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark2.guid),
+ itemType: bookmark2.type,
+ url: bookmark2.url,
+ guid: bookmark2.guid,
+ parentGuid: bookmark2.parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmark2.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ {
+ type: "bookmark-keyword-changed",
+ id: await PlacesTestUtils.promiseItemId(bookmark1.guid),
+ itemType: bookmark1.type,
+ url: bookmark1.url,
+ guid: bookmark1.guid,
+ parentGuid: bookmark1.parentGuid,
+ keyword: "",
+ lastModified: new Date(bookmark1.lastModified),
+ source: Ci.nsINavBookmarksService.SOURCE_DEFAULT,
+ isTagging: false,
+ },
+ ]);
+
+ // Now remove the bookmarks.
+ await PlacesUtils.bookmarks.remove(bookmark1);
+ await PlacesUtils.bookmarks.remove(bookmark2);
+ Assert.equal(await foreign_count("http://example.com/"), fc); // -2 bookmarks
+
+ await check_no_orphans();
+});
+
+add_task(async function test_multipleKeywordsSamePostData() {
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example.com/",
+ postData: "postData1",
+ });
+ await check_keyword(true, "http://example.com/", "keyword", "postData1");
+ // Add another keyword with same postData, should fail.
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword2",
+ url: "http://example.com/",
+ postData: "postData1",
+ });
+ await check_keyword(false, "http://example.com/", "keyword", "postData1");
+ await check_keyword(true, "http://example.com/", "keyword2", "postData1");
+
+ await PlacesUtils.keywords.remove("keyword2");
+
+ await check_no_orphans();
+});
+
+add_task(async function test_bookmarkURLChange() {
+ let fc1 = await foreign_count("http://example1.com/");
+ let fc2 = await foreign_count("http://example2.com/");
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ url: "http://example1.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesUtils.keywords.insert({
+ keyword: "keyword",
+ url: "http://example1.com/",
+ });
+
+ await check_keyword(true, "http://example1.com/", "keyword");
+ Assert.equal(await foreign_count("http://example1.com/"), fc1 + 2); // +1 bookmark +1 keyword
+
+ await PlacesUtils.bookmarks.update({
+ guid: bookmark.guid,
+ url: "http://example2.com/",
+ });
+ await promiseKeyword("keyword", "http://example2.com/");
+
+ await check_keyword(false, "http://example1.com/", "keyword");
+ await check_keyword(true, "http://example2.com/", "keyword");
+ Assert.equal(await foreign_count("http://example1.com/"), fc1); // -1 bookmark -1 keyword
+ Assert.equal(await foreign_count("http://example2.com/"), fc2 + 2); // +1 bookmark +1 keyword
+});
+
+add_task(async function test_tagDoesntPreventKeywordRemoval() {
+ await check_keyword(false, "http://example.com/", "example");
+ let fc = await foreign_count("http://example.com/");
+
+ let httpBookmark = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ Assert.equal(await foreign_count("http://example.com/"), fc + 1); // +1 bookmark
+
+ PlacesUtils.tagging.tagURI(uri("http://example.com/"), ["example_tag"]);
+ Assert.equal(await foreign_count("http://example.com/"), fc + 2); // +1 bookmark +1 tag
+
+ await PlacesUtils.keywords.insert({
+ keyword: "example",
+ url: "http://example.com/",
+ });
+ Assert.equal(await foreign_count("http://example.com/"), fc + 3); // +1 bookmark +1 tag +1 keyword
+
+ await check_keyword(true, "http://example.com/", "example");
+
+ await PlacesUtils.bookmarks.remove(httpBookmark);
+
+ await TestUtils.waitForCondition(
+ async () =>
+ !(await PlacesUtils.bookmarks.fetch({ url: "http://example.com/" })),
+ "Wait for bookmark to be removed"
+ );
+
+ await check_keyword(false, "http://example.com/", "example");
+ Assert.equal(await foreign_count("http://example.com/"), fc); // bookmark, keyword, and tag should all have been removed
+
+ await check_no_orphans();
+});
diff --git a/toolkit/components/places/tests/unit/test_lastModified.js b/toolkit/components/places/tests/unit/test_lastModified.js
new file mode 100644
index 0000000000..dc44814548
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_lastModified.js
@@ -0,0 +1,78 @@
+/* -*- 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/. */
+
+function assert_date_eq(a, b) {
+ if (typeof a != "number") {
+ a = PlacesUtils.toPRTime(a);
+ }
+ if (typeof b != "number") {
+ b = PlacesUtils.toPRTime(b);
+ }
+ Assert.equal(a, b, "The dates should match");
+}
+
+/**
+ * Test that inserting a new bookmark will set lastModified to the same
+ * values as dateAdded.
+ */
+add_task(async function test_bookmarkLastModified() {
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://www.mozilla.org/",
+ title: "itemTitle",
+ });
+
+ let guid = bookmark.guid;
+
+ // Check the bookmark from the database.
+ bookmark = await PlacesUtils.bookmarks.fetch(guid);
+
+ let dateAdded = PlacesUtils.toPRTime(bookmark.dateAdded);
+ assert_date_eq(dateAdded, bookmark.lastModified);
+
+ // Change lastModified, then change dateAdded. LastModified should be set
+ // to the new dateAdded.
+ // This could randomly fail on virtual machines due to timing issues, so
+ // we manually increase the time value. See bug 500640 for details.
+ await PlacesUtils.bookmarks.update({
+ guid,
+ lastModified: PlacesUtils.toDate(dateAdded + 1000),
+ });
+
+ bookmark = await PlacesUtils.bookmarks.fetch(guid);
+
+ assert_date_eq(bookmark.lastModified, dateAdded + 1000);
+ Assert.ok(
+ bookmark.dateAdded < bookmark.lastModified,
+ "Date added should be earlier than last modified."
+ );
+
+ await PlacesUtils.bookmarks.update({
+ guid,
+ dateAdded: PlacesUtils.toDate(dateAdded + 2000),
+ });
+
+ bookmark = await PlacesUtils.bookmarks.fetch(guid);
+
+ assert_date_eq(bookmark.dateAdded, dateAdded + 2000);
+ assert_date_eq(bookmark.dateAdded, bookmark.lastModified);
+
+ // If dateAdded is set to older than lastModified, then we shouldn't
+ // update lastModified to keep sync happy.
+ let origLastModified = bookmark.lastModified;
+
+ await PlacesUtils.bookmarks.update({
+ guid,
+ dateAdded: PlacesUtils.toDate(dateAdded - 10000),
+ });
+
+ bookmark = await PlacesUtils.bookmarks.fetch(guid);
+
+ assert_date_eq(bookmark.dateAdded, dateAdded - 10000);
+ assert_date_eq(bookmark.lastModified, origLastModified);
+
+ await PlacesUtils.bookmarks.remove(guid);
+});
diff --git a/toolkit/components/places/tests/unit/test_markpageas.js b/toolkit/components/places/tests/unit/test_markpageas.js
new file mode 100644
index 0000000000..03080f4af6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_markpageas.js
@@ -0,0 +1,48 @@
+/* -*- 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 gVisits = [
+ { url: "http://www.mozilla.com/", transition: TRANSITION_TYPED },
+ { url: "http://www.google.com/", transition: TRANSITION_BOOKMARK },
+ { url: "http://www.espn.com/", transition: TRANSITION_LINK },
+];
+
+add_task(async function test_execute() {
+ let completionPromise = new Promise(resolveCompletionPromise => {
+ let visitCount = 0;
+ function listener(aEvents) {
+ Assert.equal(aEvents.length, 1, "Right number of visits notified");
+ Assert.equal(aEvents[0].type, "page-visited");
+ let event = aEvents[0];
+ Assert.equal(event.url, gVisits[visitCount].url);
+ Assert.equal(event.transitionType, gVisits[visitCount].transition);
+ visitCount++;
+
+ if (visitCount == gVisits.length) {
+ resolveCompletionPromise();
+ PlacesObservers.removeListener(["page-visited"], listener);
+ }
+ }
+ PlacesObservers.addListener(["page-visited"], listener);
+ });
+
+ for (var visit of gVisits) {
+ if (visit.transition == TRANSITION_TYPED) {
+ PlacesUtils.history.markPageAsTyped(uri(visit.url));
+ } else if (visit.transition == TRANSITION_BOOKMARK) {
+ PlacesUtils.history.markPageAsFollowedBookmark(uri(visit.url));
+ } else {
+ // because it is a top level visit with no referrer,
+ // it will result in TRANSITION_LINK
+ }
+ await PlacesTestUtils.addVisits({
+ uri: uri(visit.url),
+ transition: visit.transition,
+ });
+ }
+
+ await completionPromise;
+});
diff --git a/toolkit/components/places/tests/unit/test_metadata.js b/toolkit/components/places/tests/unit/test_metadata.js
new file mode 100644
index 0000000000..26399f7274
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_metadata.js
@@ -0,0 +1,285 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test_metadata() {
+ await PlacesUtils.metadata.set("test/integer", 123);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("test/integer"),
+ 123,
+ "Should store new integer value"
+ );
+
+ await PlacesUtils.metadata.set("test/double", 123.45);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("test/double"),
+ 123.45,
+ "Should store new double value"
+ );
+ await PlacesUtils.metadata.set("test/double", 567.89);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("test/double"),
+ 567.89,
+ "Should update existing double value"
+ );
+
+ await PlacesUtils.metadata.set("test/boolean", false);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("test/boolean"),
+ false,
+ "Should store new Boolean value"
+ );
+ await PlacesUtils.metadata.set("test/boolean", true);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("test/boolean"),
+ true,
+ "Should update existing Boolean value"
+ );
+
+ await PlacesUtils.metadata.set("test/string", "hi");
+ Assert.equal(
+ await PlacesUtils.metadata.get("test/string"),
+ "hi",
+ "Should store new string value"
+ );
+ await PlacesUtils.metadata.cache.clear();
+ Assert.equal(
+ await PlacesUtils.metadata.get("test/string"),
+ "hi",
+ "Should return string value after clearing cache"
+ );
+
+ await Assert.rejects(
+ PlacesUtils.metadata.get("test/nonexistent"),
+ /No data stored for key test\/nonexistent/,
+ "Should reject for a non-existent key and no default value."
+ );
+ Assert.equal(
+ await PlacesUtils.metadata.get("test/nonexistent", "defaultValue"),
+ "defaultValue",
+ "Should return the default value for a non-existent key."
+ );
+
+ // Values are untyped; it's OK to store a value of a different type for the
+ // same key.
+ await PlacesUtils.metadata.set("test/string", 111);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("test/string"),
+ 111,
+ "Should replace string with integer"
+ );
+ await PlacesUtils.metadata.set("test/string", null);
+ await Assert.rejects(
+ PlacesUtils.metadata.get("test/string"),
+ /No data stored for key test\/string/,
+ "Should clear value when setting to NULL"
+ );
+
+ await PlacesUtils.metadata.delete("test/string", "test/boolean");
+ await Assert.rejects(
+ PlacesUtils.metadata.get("test/string"),
+ /No data stored for key test\/string/,
+ "Should delete string value"
+ );
+ await Assert.rejects(
+ PlacesUtils.metadata.get("test/boolean"),
+ /No data stored for key test\/boolean/,
+ "Should delete Boolean value"
+ );
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("test/integer"),
+ 123,
+ "Should keep undeleted integer value"
+ );
+
+ await PlacesTestUtils.clearMetadata();
+ await Assert.rejects(
+ PlacesUtils.metadata.get("test/integer"),
+ /No data stored for key test\/integer/,
+ "Should clear integer value"
+ );
+ await Assert.rejects(
+ PlacesUtils.metadata.get("test/double"),
+ /No data stored for key test\/double/,
+ "Should clear double value"
+ );
+});
+
+add_task(async function test_metadata_canonical_keys() {
+ await PlacesUtils.metadata.set("Test/Integer", 123);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("tEsT/integer"),
+ 123,
+ "New keys should be case-insensitive"
+ );
+ await PlacesUtils.metadata.set("test/integer", 456);
+ Assert.strictEqual(
+ await PlacesUtils.metadata.get("TEST/INTEGER"),
+ 456,
+ "Existing keys should be case-insensitive"
+ );
+
+ await Assert.rejects(
+ PlacesUtils.metadata.set("", 123),
+ /Invalid metadata key/,
+ "Should reject empty keys"
+ );
+ await Assert.rejects(
+ PlacesUtils.metadata.get(123),
+ /Invalid metadata key/,
+ "Should reject numeric keys"
+ );
+ await Assert.rejects(
+ PlacesUtils.metadata.delete(true),
+ /Invalid metadata key/,
+ "Should reject Boolean keys"
+ );
+ await Assert.rejects(
+ PlacesUtils.metadata.set({}),
+ /Invalid metadata key/,
+ "Should reject object keys"
+ );
+ await Assert.rejects(
+ PlacesUtils.metadata.get(null),
+ /Invalid metadata key/,
+ "Should reject null keys"
+ );
+ await Assert.rejects(
+ PlacesUtils.metadata.delete("!@#$"),
+ /Invalid metadata key/,
+ "Should reject keys with invalid characters"
+ );
+});
+
+add_task(async function test_metadata_blobs() {
+ let blob = new Uint8Array([1, 2, 3]);
+ await PlacesUtils.metadata.set("test/blob", blob);
+
+ let sameBlob = await PlacesUtils.metadata.get("test/blob");
+ Assert.equal(
+ ChromeUtils.getClassName(sameBlob),
+ "Uint8Array",
+ "Should cache typed array for blob value"
+ );
+ Assert.deepEqual(sameBlob, blob, "Should store new blob value");
+
+ info("Remove blob from cache");
+ await PlacesUtils.metadata.cache.clear();
+
+ let newBlob = await PlacesUtils.metadata.get("test/blob");
+ Assert.equal(
+ ChromeUtils.getClassName(newBlob),
+ "Uint8Array",
+ "Should inflate blob into typed array"
+ );
+ Assert.deepEqual(
+ newBlob,
+ blob,
+ "Should return same blob after clearing cache"
+ );
+
+ await PlacesTestUtils.clearMetadata();
+});
+
+add_task(async function test_metadata_arrays() {
+ let array = [1, 2, 3, "\u2713 \u00E0 la mode"];
+ await PlacesUtils.metadata.set("test/array", array);
+
+ let sameArray = await PlacesUtils.metadata.get("test/array");
+ Assert.ok(Array.isArray(sameArray), "Should cache array for array value");
+ Assert.deepEqual(sameArray, array, "Should store new array value");
+
+ info("Remove array from cache");
+ await PlacesUtils.metadata.cache.clear();
+
+ let newArray = await PlacesUtils.metadata.get("test/array");
+ Assert.ok(Array.isArray(newArray), "Should inflate into array");
+ Assert.deepEqual(
+ newArray,
+ array,
+ "Should return same array after clearing cache"
+ );
+
+ await PlacesTestUtils.clearMetadata();
+});
+
+add_task(async function test_metadata_objects() {
+ let object = { foo: 123, bar: "test", meow: "\u2713 \u00E0 la mode" };
+ await PlacesUtils.metadata.set("test/object", object);
+
+ let sameObject = await PlacesUtils.metadata.get("test/object");
+ Assert.equal(
+ typeof sameObject,
+ "object",
+ "Should cache object for object value"
+ );
+ Assert.deepEqual(sameObject, object, "Should store new object value");
+
+ info("Remove object from cache");
+ await PlacesUtils.metadata.cache.clear();
+
+ let newObject = await PlacesUtils.metadata.get("test/object");
+ Assert.equal(typeof newObject, "object", "Should inflate into object");
+ Assert.deepEqual(
+ newObject,
+ object,
+ "Should return same object after clearing cache"
+ );
+
+ await PlacesTestUtils.clearMetadata();
+});
+
+add_task(async function test_metadata_unparsable() {
+ await PlacesUtils.withConnectionWrapper("test_medata", db => {
+ let data = PlacesUtils.metadata._base64Encode("{hjjkhj}");
+
+ return db.execute(`
+ INSERT INTO moz_meta (key, value)
+ VALUES ("test/unparsable", "data:application/json;base64,${data}")
+ `);
+ });
+
+ await Assert.rejects(
+ PlacesUtils.metadata.get("test/unparsable"),
+ /SyntaxError: JSON.parse/,
+ "Should reject for an unparsable value with no default"
+ );
+ Assert.deepEqual(
+ await PlacesUtils.metadata.get("test/unparsable", { foo: 1 }),
+ { foo: 1 },
+ "Should return the default when encountering an unparsable value."
+ );
+
+ await PlacesTestUtils.clearMetadata();
+});
+
+add_task(async function test_metadata_setMany() {
+ await PlacesUtils.metadata.setMany(
+ new Map([
+ ["test/string", "hi"],
+ ["test/boolean", true],
+ ])
+ );
+ await PlacesUtils.metadata.set("test/string", "hi");
+ Assert.deepEqual(
+ await PlacesUtils.metadata.get("test/string"),
+ "hi",
+ "Should store new string value"
+ );
+ Assert.deepEqual(
+ await PlacesUtils.metadata.get("test/boolean"),
+ true,
+ "Should store new boolean value"
+ );
+ await PlacesUtils.metadata.cache.clear();
+ Assert.equal(
+ await PlacesUtils.metadata.get("test/string"),
+ "hi",
+ "Should return string value after clearing cache"
+ );
+ Assert.deepEqual(
+ await PlacesUtils.metadata.get("test/boolean"),
+ true,
+ "Should store new boolean value"
+ );
+ await PlacesTestUtils.clearMetadata();
+});
diff --git a/toolkit/components/places/tests/unit/test_missing_builtin_folders.js b/toolkit/components/places/tests/unit/test_missing_builtin_folders.js
new file mode 100644
index 0000000000..a7d36d27da
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_missing_builtin_folders.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This file tests that a missing built-in folders (child of root) are correctly
+ * fixed when the database is loaded.
+ */
+
+const ALL_ROOT_GUIDS = [
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.tagsGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+];
+
+add_task(async function setup() {
+ await setupPlacesDatabase([
+ "migration",
+ `places_v${Ci.nsINavHistoryService.DATABASE_SCHEMA_VERSION}.sqlite`,
+ ]);
+
+ // Prepare database contents by removing the tolbar and mobile folders.
+ let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
+ let db = await Sqlite.openConnection({ path });
+ await db.execute(
+ `
+ DELETE FROM moz_bookmarks WHERE guid IN(:toolbar, :mobile)
+ `,
+ {
+ toolbar: PlacesUtils.bookmarks.toolbarGuid,
+ mobile: PlacesUtils.bookmarks.mobileGuid,
+ }
+ );
+ await db.close();
+});
+
+add_task(async function test_database_recreates_roots() {
+ Assert.ok(
+ PlacesUtils.history.databaseStatus ==
+ PlacesUtils.history.DATABASE_STATUS_OK ||
+ PlacesUtils.history.databaseStatus ==
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED,
+ "Should successfully access the database for the first time"
+ );
+
+ let db = await PlacesUtils.promiseDBConnection();
+ let rootId = await PlacesTestUtils.promiseItemId(
+ PlacesUtils.bookmarks.rootGuid
+ );
+ for (let guid of ALL_ROOT_GUIDS) {
+ let rows = await db.execute(
+ `
+ SELECT id, parent FROM moz_bookmarks
+ WHERE guid = :guid
+ `,
+ { guid }
+ );
+
+ Assert.equal(rows.length, 1, "Should have exactly one row for the root");
+
+ Assert.equal(
+ rows[0].getResultByName("parent"),
+ rootId,
+ "Should have been created with the correct parent"
+ );
+
+ let root = await PlacesUtils.bookmarks.fetch(guid);
+
+ Assert.equal(root.guid, guid, "GUIDs should match");
+ Assert.equal(
+ root.parentGuid,
+ PlacesUtils.bookmarks.rootGuid,
+ "Should have the correct parent GUID"
+ );
+ Assert.equal(
+ root.type,
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Should have the correct type"
+ );
+
+ let id = rows[0].getResultByName("id");
+ Assert.equal(
+ await PlacesTestUtils.promiseItemId(guid),
+ id,
+ "Should return the correct id from promiseItemId"
+ );
+ Assert.equal(
+ await PlacesTestUtils.promiseItemGuid(id),
+ guid,
+ "Should return the correct guid from promiseItemGuid"
+ );
+ }
+
+ let rows = await db.execute(
+ `
+ SELECT 1 FROM moz_bookmarks
+ WHERE parent = (SELECT id from moz_bookmarks WHERE guid = :guid)
+ `,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ }
+ );
+
+ Assert.equal(
+ rows.length,
+ ALL_ROOT_GUIDS.length,
+ "Root folder should have the expected number of children"
+ );
+});
diff --git a/toolkit/components/places/tests/unit/test_missing_root_folder.js b/toolkit/components/places/tests/unit/test_missing_root_folder.js
new file mode 100644
index 0000000000..59f8814d03
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_missing_root_folder.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * This file tests that a missing root folder is correctly fixed when the
+ * database is loaded.
+ */
+
+const ALL_ROOT_GUIDS = [
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.tagsGuid,
+ PlacesUtils.bookmarks.mobileGuid,
+];
+
+add_task(async function setup() {
+ await setupPlacesDatabase([
+ "migration",
+ `places_v${Ci.nsINavHistoryService.DATABASE_SCHEMA_VERSION}.sqlite`,
+ ]);
+
+ // Prepare database contents by removing the root folder.
+ let path = PathUtils.join(PathUtils.profileDir, DB_FILENAME);
+ let db = await Sqlite.openConnection({ path });
+ await db.execute(
+ `
+ DELETE FROM moz_bookmarks WHERE guid = :guid
+ `,
+ {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ }
+ );
+ await db.close();
+});
+
+add_task(async function test_database_recreates_roots() {
+ Assert.ok(
+ PlacesUtils.history.databaseStatus ==
+ PlacesUtils.history.DATABASE_STATUS_OK ||
+ PlacesUtils.history.databaseStatus ==
+ PlacesUtils.history.DATABASE_STATUS_UPGRADED,
+ "Should successfully access the database for the first time"
+ );
+
+ let db = await PlacesUtils.promiseDBConnection();
+
+ let rows = await db.execute(
+ `
+ SELECT id, parent, type FROM moz_bookmarks
+ WHERE guid = :guid
+ `,
+ { guid: PlacesUtils.bookmarks.rootGuid }
+ );
+
+ Assert.equal(rows.length, 1, "Should have added exactly one root");
+ Assert.greaterOrEqual(
+ rows[0].getResultByName("id"),
+ 1,
+ "Should have a valid root Id"
+ );
+ Assert.equal(
+ rows[0].getResultByName("parent"),
+ 0,
+ "Should have a parent of id 0"
+ );
+ Assert.equal(
+ rows[0].getResultByName("type"),
+ PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Should have a type of folder"
+ );
+
+ let id = rows[0].getResultByName("id");
+ Assert.equal(
+ await PlacesTestUtils.promiseItemId(PlacesUtils.bookmarks.rootGuid),
+ id,
+ "Should return the correct id from promiseItemId"
+ );
+ Assert.equal(
+ await PlacesTestUtils.promiseItemGuid(id),
+ PlacesUtils.bookmarks.rootGuid,
+ "Should return the correct guid from promiseItemGuid"
+ );
+
+ // Note: Currently we do not fix the parent of the folders on initial startup.
+ // There is a maintenance task that will do it, hence we don't check the parents
+ // here, just that the built-in folders correctly exist and haven't been
+ // duplicated.
+ for (let guid of ALL_ROOT_GUIDS) {
+ rows = await db.execute(
+ `
+ SELECT id FROM moz_bookmarks
+ WHERE guid = :guid
+ `,
+ { guid }
+ );
+
+ Assert.equal(rows.length, 1, "Should have exactly one row for the root");
+
+ let root = await PlacesUtils.bookmarks.fetch(guid);
+
+ Assert.equal(root.guid, guid, "GUIDs should match");
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_multi_observation.js b/toolkit/components/places/tests/unit/test_multi_observation.js
new file mode 100644
index 0000000000..d56eabf8f1
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_multi_observation.js
@@ -0,0 +1,384 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test whether registered listener capture proper events.
+// We test the following combinations.
+// * Listener: listen to single/multi event(s)
+// * Event: fire single/multi type of event(s)
+// * Timing: fire event(s) at same time/separately
+// And also test notifying empty events.
+
+add_task(async () => {
+ info("Test for listening to single event and firing single event");
+
+ const observer = startObservation(["page-visited"]);
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "test",
+ url: "http://example.com/test",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const expectedFiredEvents = [
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/test",
+ },
+ ],
+ ];
+ assertFiredEvents(observer.firedEvents, expectedFiredEvents);
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async () => {
+ info("Test for listening to multi events with firing single event");
+
+ const observer = startObservation(["page-visited", "page-title-changed"]);
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "test",
+ url: "http://example.com/test",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const expectedFiredEvents = [
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/test",
+ },
+ ],
+ ];
+ assertFiredEvents(observer.firedEvents, expectedFiredEvents);
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async () => {
+ info(
+ "Test for listening to single event with firing multi events at same time"
+ );
+
+ const vistedObserver = startObservation(["page-visited"]);
+ const titleChangedObserver = startObservation(["page-title-changed"]);
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "will change",
+ url: "http://example.com/title",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ {
+ title: "changed",
+ url: "http://example.com/title",
+ referrer: "http://example.com/title",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ {
+ title: "another",
+ url: "http://example.com/another",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const expectedVisitedFiredEvents = [
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/title",
+ },
+ {
+ type: "page-visited",
+ url: "http://example.com/title",
+ },
+ {
+ type: "page-visited",
+ url: "http://example.com/another",
+ },
+ ],
+ ];
+ assertFiredEvents(vistedObserver.firedEvents, expectedVisitedFiredEvents);
+
+ const expectedTitleChangedFiredEvents = [
+ [
+ {
+ type: "page-title-changed",
+ url: "http://example.com/title",
+ title: "changed",
+ },
+ ],
+ ];
+ assertFiredEvents(
+ titleChangedObserver.firedEvents,
+ expectedTitleChangedFiredEvents
+ );
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async () => {
+ info(
+ "Test for listening to single event with firing multi events separately"
+ );
+
+ const observer = startObservation(["page-visited"]);
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "test",
+ url: "http://example.com/test",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder",
+ });
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "another",
+ url: "http://example.com/another",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const expectedFiredEvents = [
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/test",
+ },
+ ],
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/another",
+ },
+ ],
+ ];
+ assertFiredEvents(observer.firedEvents, expectedFiredEvents);
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+});
+
+add_task(async () => {
+ info("Test for listening to multi events with firing single event");
+
+ const observer = startObservation([
+ "page-visited",
+ "page-title-changed",
+ "bookmark-added",
+ ]);
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "test",
+ url: "http://example.com/test",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const expectedFiredEvents = [
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/test",
+ },
+ ],
+ ];
+ assertFiredEvents(observer.firedEvents, expectedFiredEvents);
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async () => {
+ info(
+ "Test for listening to multi events with firing multi events at same time"
+ );
+
+ const observer = startObservation([
+ "page-visited",
+ "page-title-changed",
+ "bookmark-added",
+ ]);
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "will change",
+ url: "http://example.com/title",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ {
+ title: "changed",
+ url: "http://example.com/title",
+ referrer: "http://example.com/title",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ {
+ title: "another",
+ url: "http://example.com/another",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const expectedFiredEvents = [
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/title",
+ },
+ {
+ type: "page-visited",
+ url: "http://example.com/title",
+ },
+ {
+ type: "page-title-changed",
+ url: "http://example.com/title",
+ title: "changed",
+ },
+ {
+ type: "page-visited",
+ url: "http://example.com/another",
+ },
+ ],
+ ];
+ assertFiredEvents(observer.firedEvents, expectedFiredEvents);
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async () => {
+ info(
+ "Test for listening to multi events with firing multi events separately"
+ );
+
+ const observer = startObservation([
+ "page-visited",
+ "page-title-changed",
+ "bookmark-added",
+ ]);
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "will change",
+ url: "http://example.com/title",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "a folder",
+ });
+
+ await PlacesUtils.history.insertMany([
+ {
+ title: "changed",
+ url: "http://example.com/title",
+ referrer: "http://example.com/title",
+ visits: [{ transition: TRANSITION_LINK }],
+ },
+ ]);
+
+ const expectedFiredEvents = [
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/title",
+ },
+ ],
+ [
+ {
+ type: "bookmark-added",
+ title: "a folder",
+ },
+ ],
+ [
+ {
+ type: "page-visited",
+ url: "http://example.com/title",
+ },
+ {
+ type: "page-title-changed",
+ url: "http://example.com/title",
+ title: "changed",
+ },
+ ],
+ ];
+ assertFiredEvents(observer.firedEvents, expectedFiredEvents);
+
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+});
+
+add_task(async function test_empty_notifications_array() {
+ info("Test whether listener does not receive empty events");
+
+ if (AppConstants.DEBUG) {
+ info(
+ "Ignore this test since we added a MOZ_ASSERT for empty events in debug build"
+ );
+ return;
+ }
+
+ const observer = startObservation(["page-visited"]);
+ PlacesObservers.notifyListeners([]);
+ Assert.equal(observer.firedEvents.length, 0, "Listener does not receive any");
+});
+
+function startObservation(targets) {
+ const observer = {
+ firedEvents: [],
+ handle(events) {
+ this.firedEvents.push(events);
+ },
+ };
+
+ PlacesObservers.addListener(targets, observer.handle.bind(observer));
+
+ return observer;
+}
+
+function assertFiredEvents(firedEvents, expectedFiredEvents) {
+ Assert.equal(
+ firedEvents.length,
+ expectedFiredEvents.length,
+ "Number events fired is correct"
+ );
+
+ for (let i = 0; i < firedEvents.length; i++) {
+ info(`Check firedEvents[${i}]`);
+ const events = firedEvents[i];
+ const expectedEvents = expectedFiredEvents[i];
+ assertEvents(events, expectedEvents);
+ }
+}
+
+function assertEvents(events, expectedEvents) {
+ Assert.equal(
+ events.length,
+ expectedEvents.length,
+ "Number events is correct"
+ );
+
+ for (let i = 0; i < events.length; i++) {
+ info(`Check events[${i}]`);
+ const event = events[i];
+ const expectedEvent = expectedEvents[i];
+
+ for (let field in expectedEvent) {
+ Assert.equal(event[field], expectedEvent[field], `${field} is correct`);
+ }
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_multi_word_tags.js b/toolkit/components/places/tests/unit/test_multi_word_tags.js
new file mode 100644
index 0000000000..17a68cf972
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_multi_word_tags.js
@@ -0,0 +1,147 @@
+/* -*- 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 history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+// Get tagging service
+try {
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService(
+ Ci.nsITaggingService
+ );
+} catch (ex) {
+ do_throw("Could not get tagging service\n");
+}
+
+add_task(async function run_test() {
+ var uri1 = uri("http://site.tld/1");
+ var uri2 = uri("http://site.tld/2");
+ var uri3 = uri("http://site.tld/3");
+ var uri4 = uri("http://site.tld/4");
+ var uri5 = uri("http://site.tld/5");
+ var uri6 = uri("http://site.tld/6");
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ { url: uri1 },
+ { url: uri2 },
+ { url: uri3 },
+ { url: uri4 },
+ { url: uri5 },
+ { url: uri6 },
+ ],
+ });
+
+ tagssvc.tagURI(uri1, ["foo"]);
+ tagssvc.tagURI(uri2, ["bar"]);
+ tagssvc.tagURI(uri3, ["cheese"]);
+ tagssvc.tagURI(uri4, ["foo bar"]);
+ tagssvc.tagURI(uri5, ["bar cheese"]);
+ tagssvc.tagURI(uri6, ["foo bar cheese"]);
+
+ // Search for "item", should get one result
+ var options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ var query = histsvc.getNewQuery();
+ query.searchTerms = "foo";
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 3);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/1");
+ Assert.equal(root.getChild(1).uri, "http://site.tld/4");
+ Assert.equal(root.getChild(2).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 4);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/2");
+ Assert.equal(root.getChild(1).uri, "http://site.tld/4");
+ Assert.equal(root.getChild(2).uri, "http://site.tld/5");
+ Assert.equal(root.getChild(3).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 3);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/3");
+ Assert.equal(root.getChild(1).uri, "http://site.tld/5");
+ Assert.equal(root.getChild(2).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "foo bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 2);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/4");
+ Assert.equal(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "bar foo";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 2);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/4");
+ Assert.equal(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "bar cheese";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 2);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/5");
+ Assert.equal(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 2);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/5");
+ Assert.equal(root.getChild(1).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "foo bar cheese";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese foo bar";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/6");
+ root.containerOpen = false;
+
+ query.searchTerms = "cheese bar foo";
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+ Assert.equal(root.getChild(0).uri, "http://site.tld/6");
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_nested_notifications.js b/toolkit/components/places/tests/unit/test_nested_notifications.js
new file mode 100644
index 0000000000..35bcd043d7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_nested_notifications.js
@@ -0,0 +1,186 @@
+/* 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 for nested notifications of Places events.
+ * In this test, we check behavior of listeners of Places event upon firing nested
+ * notification from inside of listener that received notifications.
+ */
+add_task(async function () {
+ // We prepare 6 listeners for the test.
+ // 1. Listener that added before root notification.
+ const addRoot = new Observer();
+ // 2. Listener that added before root notification
+ // but removed before first nest notification.
+ const addRootRemoveFirst = new Observer();
+ // 3. Listener that added before root notification
+ // but removed before second nest notification.
+ const addRootRemoveSecond = new Observer();
+ // 4. Listener that added before first nest notification.
+ const addFirst = new Observer();
+ // 5. Listener that added before first nest notification
+ // but removed before second nest notification.
+ const addFirstRemoveSecond = new Observer();
+ // 6. Listener that added before second nest notification.
+ const addSecond = new Observer();
+
+ // This is a listener listened the root notification
+ // and do what we have to do for test in the first nest.
+ const firstNestOperator = () => {
+ info("Start to operate at first nest");
+
+ // Remove itself to avoid listening more.
+ removePlacesListener(firstNestOperator);
+
+ info("Add/Remove test listeners at first nest");
+ removePlacesListener(addRootRemoveFirst.handle);
+ addPlacesListener(addFirst.handle);
+ addPlacesListener(addFirstRemoveSecond.handle);
+
+ // Add second nest operator.
+ addPlacesListener(secondNestOperator);
+
+ info("Send notification at first nest");
+ notifyPlacesEvent("first");
+ };
+
+ // This is a listener listened the first nest notification
+ // and do what we have to do for test in the second nest.
+ const secondNestOperator = () => {
+ info("Start to operate at second nest");
+
+ // Remove itself to avoid listening more.
+ removePlacesListener(secondNestOperator);
+
+ info("Add/Remove test listeners at second nest");
+ removePlacesListener(addRootRemoveSecond.handle);
+ removePlacesListener(addFirstRemoveSecond.handle);
+ addPlacesListener(addSecond.handle);
+
+ info("Send notification at second nest");
+ notifyPlacesEvent("second");
+ };
+
+ info("Add test listeners that handle notification sent at root");
+ addPlacesListener(addRoot.handle);
+ addPlacesListener(addRootRemoveFirst.handle);
+ addPlacesListener(addRootRemoveSecond.handle);
+
+ // Add first nest operator.
+ addPlacesListener(firstNestOperator);
+
+ info("Send notification at root");
+ notifyPlacesEvent("root");
+
+ info("Check whether or not test listeners could get expected notifications");
+ assertNotifications(addRoot.notifications, [
+ [{ guid: "root" }],
+ [{ guid: "first" }],
+ [{ guid: "second" }],
+ ]);
+ assertNotifications(addRootRemoveFirst.notifications, [[{ guid: "root" }]]);
+ assertNotifications(addRootRemoveSecond.notifications, [
+ [{ guid: "root" }],
+ [{ guid: "first" }],
+ ]);
+ assertNotifications(addFirst.notifications, [
+ [{ guid: "first" }],
+ [{ guid: "second" }],
+ ]);
+ assertNotifications(addFirstRemoveSecond.notifications, [
+ [{ guid: "first" }],
+ ]);
+ assertNotifications(addSecond.notifications, [[{ guid: "second" }]]);
+});
+
+function addPlacesListener(listener) {
+ PlacesObservers.addListener(["bookmark-added"], listener);
+}
+
+function removePlacesListener(listener) {
+ PlacesObservers.removeListener(["bookmark-added"], listener);
+}
+
+function notifyPlacesEvent(guid) {
+ PlacesObservers.notifyListeners([
+ new PlacesBookmarkAddition({
+ dateAdded: 0,
+ guid,
+ id: -1,
+ index: 0,
+ isTagging: false,
+ itemType: 1,
+ parentGuid: "fake",
+ parentId: -2,
+ source: 0,
+ title: guid,
+ tags: "tags",
+ url: `http://example.com/${guid}`,
+ frecency: 0,
+ hidden: false,
+ visitCount: 0,
+ lastVisitDate: 0,
+ targetFolderGuid: null,
+ targetFolderItemId: -1,
+ targetFolderTitle: null,
+ }),
+ ]);
+}
+
+class Observer {
+ constructor() {
+ this.notifications = [];
+ this.handle = this.handle.bind(this);
+ }
+
+ handle(events) {
+ this.notifications.push(events);
+ }
+}
+
+/**
+ * Assert notifications the observer received.
+ *
+ * @param Array - notifications
+ * @param Array - expectedNotifications
+ */
+function assertNotifications(notifications, expectedNotifications) {
+ Assert.equal(
+ notifications.length,
+ expectedNotifications.length,
+ "Number of notifications is correct"
+ );
+
+ for (let i = 0; i < notifications.length; i++) {
+ info(`Check notifications[${i}]`);
+ const placesEvents = notifications[i];
+ const expectedPlacesEvents = expectedNotifications[i];
+ assertPlacesEvents(placesEvents, expectedPlacesEvents);
+ }
+}
+
+/**
+ * Assert Places events.
+ * This function checks given expected event field only.
+ *
+ * @param Array - events
+ * @param Array - expectedEvents
+ */
+function assertPlacesEvents(events, expectedEvents) {
+ Assert.equal(
+ events.length,
+ expectedEvents.length,
+ "Number of Places events is correct"
+ );
+
+ for (let i = 0; i < events.length; i++) {
+ info(`Check Places events[${i}]`);
+ const event = events[i];
+ const expectedEvent = expectedEvents[i];
+
+ for (let field in expectedEvent) {
+ Assert.equal(event[field], expectedEvent[field], `${field} is correct`);
+ }
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js
new file mode 100644
index 0000000000..e98cdbac79
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js
@@ -0,0 +1,284 @@
+/* -*- 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 resultObserver = {
+ insertedNode: null,
+ nodeInserted(parent, node, newIndex) {
+ this.insertedNode = node;
+ },
+ removedNode: null,
+ nodeRemoved(parent, node, oldIndex) {
+ this.removedNode = node;
+ },
+
+ newTitle: "",
+ nodeChangedByTitle: null,
+ nodeTitleChanged(node, newTitle) {
+ this.nodeChangedByTitle = node;
+ this.newTitle = newTitle;
+ },
+
+ newAccessCount: 0,
+ newTime: 0,
+ nodeChangedByHistoryDetails: null,
+ nodeHistoryDetailsChanged(node, oldVisitDate, oldVisitCount) {
+ this.nodeChangedByHistoryDetails = node;
+ this.newTime = node.time;
+ this.newAccessCount = node.accessCount;
+ },
+
+ movedNode: null,
+ nodeMoved(node, oldParent, oldIndex, newParent, newIndex) {
+ this.movedNode = node;
+ },
+ openedContainer: null,
+ closedContainer: null,
+ containerStateChanged(aNode, aOldState, aNewState) {
+ if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED) {
+ this.openedContainer = aNode;
+ } else if (aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED) {
+ this.closedContainer = aNode;
+ }
+ },
+ invalidatedContainer: null,
+ invalidateContainer(node) {
+ this.invalidatedContainer = node;
+ },
+ sortingMode: null,
+ sortingChanged(sortingMode) {
+ this.sortingMode = sortingMode;
+ },
+ inBatchMode: false,
+ batchingCallCount: 0,
+ batching(aToggleMode) {
+ Assert.notEqual(this.inBatchMode, aToggleMode);
+ this.inBatchMode = aToggleMode;
+ this.batchingCallCount++;
+ },
+ result: null,
+ reset() {
+ this.insertedNode = null;
+ this.removedNode = null;
+ this.nodeChangedByTitle = null;
+ this.nodeChangedByHistoryDetails = null;
+ this.replacedNode = null;
+ this.movedNode = null;
+ this.openedContainer = null;
+ this.closedContainer = null;
+ this.invalidatedContainer = null;
+ this.sortingMode = null;
+ this.inBatchMode = false;
+ this.batchingCallCount = 0;
+ },
+};
+
+var testURI = uri("http://mozilla.com");
+
+add_task(async function check_history_query() {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ let query = PlacesUtils.history.getNewQuery();
+ let result = PlacesUtils.history.executeQuery(query, options);
+ result.addObserver(resultObserver);
+ let root = result.root;
+ root.containerOpen = true;
+
+ Assert.notEqual(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.nodeInserted
+ // add a visit
+ await PlacesTestUtils.addVisits(testURI);
+ Assert.equal(testURI.spec, resultObserver.insertedNode.uri);
+
+ // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
+ // adding a visit causes nodeHistoryDetailsChanged for the folder
+ Assert.equal(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+
+ // nsINavHistoryResultObserver.itemTitleChanged for a leaf node
+ await PlacesTestUtils.addVisits({ uri: testURI, title: "baz" });
+ Assert.equal(resultObserver.nodeChangedByTitle.title, "baz");
+
+ // nsINavHistoryResultObserver.nodeRemoved
+ let removedURI = uri("http://google.com");
+ await PlacesTestUtils.addVisits(removedURI);
+ await PlacesUtils.history.remove(removedURI);
+ Assert.equal(removedURI.spec, resultObserver.removedNode.uri);
+
+ // nsINavHistoryResultObserver.invalidateContainer
+ await PlacesUtils.history.removeByFilter({ host: "mozilla.com" });
+ // This test is disabled for bug 1089691. It is failing bcause the new API
+ // doesn't send batching notifications and thus the result doesn't invalidate
+ // the whole container.
+ // Assert.equal(root.uri, resultObserver.invalidatedContainer.uri);
+
+ // nsINavHistoryResultObserver.sortingChanged
+ resultObserver.invalidatedContainer = null;
+ result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ Assert.equal(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
+ Assert.equal(resultObserver.invalidatedContainer, result.root);
+
+ // nsINavHistoryResultObserver.invalidateContainer
+ await PlacesUtils.history.clear();
+ Assert.equal(root.uri, resultObserver.invalidatedContainer.uri);
+
+ root.containerOpen = false;
+ Assert.equal(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(async function check_bookmarks_query() {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.menuGuid]);
+ let result = PlacesUtils.history.executeQuery(query, options);
+ result.addObserver(resultObserver);
+ let root = result.root;
+ root.containerOpen = true;
+
+ Assert.notEqual(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.nodeInserted
+ // add a bookmark
+ let testBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: testURI,
+ title: "foo",
+ });
+ Assert.equal("foo", resultObserver.insertedNode.title);
+ Assert.equal(testURI.spec, resultObserver.insertedNode.uri);
+
+ // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
+ // adding a visit causes nodeHistoryDetailsChanged for the folder
+ Assert.equal(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+
+ // nsINavHistoryResultObserver.nodeTitleChanged for a leaf node
+ await PlacesUtils.bookmarks.update({
+ guid: testBookmark.guid,
+ title: "baz",
+ });
+ Assert.equal(resultObserver.nodeChangedByTitle.title, "baz");
+ Assert.equal(resultObserver.newTitle, "baz");
+
+ let testBookmark2 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://google.com",
+ title: "foo",
+ });
+
+ await PlacesUtils.bookmarks.update({
+ guid: testBookmark2.guid,
+ index: 0,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+ Assert.equal(resultObserver.movedNode.bookmarkGuid, testBookmark2.guid);
+
+ // nsINavHistoryResultObserver.nodeRemoved
+ await PlacesUtils.bookmarks.remove(testBookmark2.guid);
+ Assert.equal(testBookmark2.guid, resultObserver.removedNode.bookmarkGuid);
+
+ // XXX nsINavHistoryResultObserver.invalidateContainer
+
+ // nsINavHistoryResultObserver.sortingChanged
+ resultObserver.invalidatedContainer = null;
+ result.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING;
+ Assert.equal(
+ resultObserver.sortingMode,
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING
+ );
+ Assert.equal(resultObserver.invalidatedContainer, result.root);
+
+ root.containerOpen = false;
+ Assert.equal(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(async function check_mixed_query() {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ let result = PlacesUtils.history.executeQuery(query, options);
+ result.addObserver(resultObserver);
+ let root = result.root;
+ root.containerOpen = true;
+
+ Assert.notEqual(resultObserver.openedContainer, null);
+
+ root.containerOpen = false;
+ Assert.equal(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(async function check_a_batch_process() {
+ const options = PlacesUtils.history.getNewQueryOptions();
+ const query = PlacesUtils.history.getNewQuery();
+ const result = PlacesUtils.history.executeQuery(query, options);
+ result.addObserver(resultObserver);
+
+ info("Check initial state");
+ Assert.equal(resultObserver.inBatchMode, false);
+ Assert.equal(resultObserver.batchingCallCount, 0);
+
+ info("Check whether batching is called when call onBeginUpdateBatch");
+ result.onBeginUpdateBatch();
+ Assert.equal(resultObserver.inBatchMode, true);
+ Assert.equal(resultObserver.batchingCallCount, 1);
+
+ info("Check whether batching is called when call onEndUpdateBatch");
+ result.onEndUpdateBatch();
+ Assert.equal(resultObserver.inBatchMode, false);
+ Assert.equal(resultObserver.batchingCallCount, 2);
+
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(async function check_multi_batch_processes() {
+ const options = PlacesUtils.history.getNewQueryOptions();
+ const query = PlacesUtils.history.getNewQuery();
+ const result = PlacesUtils.history.executeQuery(query, options);
+ result.addObserver(resultObserver);
+
+ info("Check initial state");
+ Assert.equal(resultObserver.inBatchMode, false);
+ Assert.equal(resultObserver.batchingCallCount, 0);
+
+ info("Check whether batching is called when calling onBeginUpdateBatch");
+ result.onBeginUpdateBatch();
+ Assert.equal(resultObserver.inBatchMode, true);
+ Assert.equal(resultObserver.batchingCallCount, 1);
+
+ info(
+ "Check whether batching is not called when calling onBeginUpdateBatch again"
+ );
+ result.onBeginUpdateBatch();
+ Assert.equal(resultObserver.inBatchMode, true);
+ Assert.equal(resultObserver.batchingCallCount, 1);
+
+ info(
+ "Check whether batching is not called until calling onEndUpdateBatch the same number times that onBeginUpdateBatch is called"
+ );
+ result.onEndUpdateBatch();
+ Assert.equal(resultObserver.inBatchMode, true);
+ Assert.equal(resultObserver.batchingCallCount, 1);
+
+ info(
+ "Check whether batching is called when calling onEndUpdateBatch the same number times"
+ );
+ result.onEndUpdateBatch();
+ Assert.equal(resultObserver.inBatchMode, false);
+ Assert.equal(resultObserver.batchingCallCount, 2);
+
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ await PlacesTestUtils.promiseAsyncUpdates();
+});
diff --git a/toolkit/components/places/tests/unit/test_null_interfaces.js b/toolkit/components/places/tests/unit/test_null_interfaces.js
new file mode 100644
index 0000000000..f5608285d6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_null_interfaces.js
@@ -0,0 +1,105 @@
+/* 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 bug 489872 to make sure passing nulls to nsNavHistory doesn't crash.
+ */
+
+// Make an array of services to test, each specifying a class id, interface
+// and an array of function names that don't throw when passed nulls
+var testServices = [
+ [
+ "browser/nav-history-service;1",
+ ["nsINavHistoryService"],
+ [
+ "queryStringToQuery",
+ "removePagesByTimeframe",
+ "removePagesFromHost",
+ "getObservers",
+ ],
+ ],
+ [
+ "browser/nav-bookmarks-service;1",
+ ["nsINavBookmarksService"],
+ ["createFolder", "getObservers"],
+ ],
+ ["browser/favicon-service;1", ["nsIFaviconService"], []],
+ ["browser/tagging-service;1", ["nsITaggingService"], []],
+];
+info(testServices.join("\n"));
+
+function run_test() {
+ for (let [cid, ifaces, nothrow] of testServices) {
+ info(`Running test with ${cid} ${ifaces.join(", ")} ${nothrow}`);
+ let s = Cc["@mozilla.org/" + cid].getService(Ci.nsISupports);
+ for (let iface of ifaces) {
+ s.QueryInterface(Ci[iface]);
+ }
+
+ let okName = function (name) {
+ info(`Checking if function is okay to test: ${name}`);
+ let func = s[name];
+
+ let mesg = "";
+ if (typeof func != "function") {
+ mesg = "Not a function!";
+ } else if (!func.length) {
+ mesg = "No args needed!";
+ } else if (name == "QueryInterface") {
+ mesg = "Ignore QI!";
+ }
+
+ if (mesg) {
+ info(`${mesg} Skipping: ${name}`);
+ return false;
+ }
+
+ return true;
+ };
+
+ info(`Generating an array of functions to test service: ${s}`);
+ for (let n of Object.keys(s)
+ .filter(i => okName(i))
+ .sort()) {
+ info(`\nTesting ${ifaces.join(", ")} function with null args: ${n}`);
+
+ let func = s[n];
+ let num = func.length;
+ info(`Generating array of nulls for #args: ${num}`);
+ let args = Array(num).fill(null);
+
+ let tryAgain = true;
+ while (tryAgain) {
+ try {
+ info(`Calling with args: ${JSON.stringify(args)}`);
+ func.apply(s, args);
+
+ info(
+ `The function did not throw! Is it one of the nothrow? ${nothrow}`
+ );
+ Assert.notEqual(nothrow.indexOf(n), -1);
+
+ info("Must have been an expected nothrow, so no need to try again");
+ tryAgain = false;
+ } catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ info(`Caught an expected exception: ${ex.name}`);
+ info("Moving on to the next test..");
+ tryAgain = false;
+ } else if (ex.result == Cr.NS_ERROR_XPC_NEED_OUT_OBJECT) {
+ let pos = Number(ex.message.match(/object arg (\d+)/)[1]);
+ info(`Function call expects an out object at ${pos}`);
+ args[pos] = {};
+ } else if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
+ info(`Method not implemented exception: ${ex.name}`);
+ info("Moving on to the next test..");
+ tryAgain = false;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_origins.js b/toolkit/components/places/tests/unit/test_origins.js
new file mode 100644
index 0000000000..67b6d59c7d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_origins.js
@@ -0,0 +1,1113 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Makes sure the moz_origins table and origin frecency stats are updated
+// correctly.
+
+"use strict";
+
+// Visiting a URL with a new origin should immediately update moz_origins.
+add_task(async function visit() {
+ await checkDB([]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Repeatedly visiting a URL with an initially new origin should update
+// moz_origins (with the correct frecency).
+add_task(async function visitRepeatedly() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://example.com/" },
+ { uri: "http://example.com/" },
+ ]);
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Same as previous, but visits are added sequentially.
+add_task(async function visitRepeatedlySequential() {
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// After removing an origin's URLs, visiting a URL with the origin should
+// immediately update moz_origins.
+add_task(async function vistAfterDelete() {
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/" }]);
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Visiting different URLs with the same origin should update moz_origins, and
+// moz_origins.frecency should be the sum of the URL frecencies.
+add_task(async function visitDifferentURLsSameOrigin() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/1" },
+ { uri: "http://example.com/2" },
+ { uri: "http://example.com/3" },
+ ]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/1", "http://example.com/2", "http://example.com/3"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/1");
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/2", "http://example.com/3"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/2");
+ await checkDB([["http://", "example.com", ["http://example.com/3"]]]);
+ await PlacesUtils.history.remove("http://example.com/3");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Same as previous, but visits are added sequentially.
+add_task(async function visitDifferentURLsSameOriginSequential() {
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/1" }]);
+ await checkDB([["http://", "example.com", ["http://example.com/1"]]]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/2" }]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/1", "http://example.com/2"],
+ ],
+ ]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/3" }]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/1", "http://example.com/2", "http://example.com/3"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/1");
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/2", "http://example.com/3"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/2");
+ await checkDB([["http://", "example.com", ["http://example.com/3"]]]);
+ await PlacesUtils.history.remove("http://example.com/3");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Repeatedly visiting different URLs with the same origin should update
+// moz_origins (with the correct frecencies), and moz_origins.frecency should be
+// the sum of the URL frecencies.
+add_task(async function visitDifferentURLsSameOriginRepeatedly() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/1" },
+ { uri: "http://example.com/1" },
+ { uri: "http://example.com/1" },
+ { uri: "http://example.com/2" },
+ { uri: "http://example.com/2" },
+ { uri: "http://example.com/3" },
+ ]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/1", "http://example.com/2", "http://example.com/3"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/1");
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/2", "http://example.com/3"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/2");
+ await checkDB([["http://", "example.com", ["http://example.com/3"]]]);
+ await PlacesUtils.history.remove("http://example.com/3");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Visiting URLs with different origins should update moz_origins.
+add_task(async function visitDifferentOrigins() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example1.com/" },
+ { uri: "http://example2.com/" },
+ { uri: "http://example3.com/" },
+ ]);
+ await checkDB([
+ ["http://", "example1.com", ["http://example1.com/"]],
+ ["http://", "example2.com", ["http://example2.com/"]],
+ ["http://", "example3.com", ["http://example3.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/");
+ await checkDB([
+ ["http://", "example2.com", ["http://example2.com/"]],
+ ["http://", "example3.com", ["http://example3.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/");
+ await checkDB([["http://", "example3.com", ["http://example3.com/"]]]);
+ await PlacesUtils.history.remove("http://example3.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Same as previous, but visits are added sequentially.
+add_task(async function visitDifferentOriginsSequential() {
+ await PlacesTestUtils.addVisits([{ uri: "http://example1.com/" }]);
+ await checkDB([["http://", "example1.com", ["http://example1.com/"]]]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example2.com/" }]);
+ await checkDB([
+ ["http://", "example1.com", ["http://example1.com/"]],
+ ["http://", "example2.com", ["http://example2.com/"]],
+ ]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example3.com/" }]);
+ await checkDB([
+ ["http://", "example1.com", ["http://example1.com/"]],
+ ["http://", "example2.com", ["http://example2.com/"]],
+ ["http://", "example3.com", ["http://example3.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/");
+ await checkDB([
+ ["http://", "example2.com", ["http://example2.com/"]],
+ ["http://", "example3.com", ["http://example3.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/");
+ await checkDB([["http://", "example3.com", ["http://example3.com/"]]]);
+ await PlacesUtils.history.remove("http://example3.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Repeatedly visiting URLs with different origins should update moz_origins
+// (with the correct frecencies).
+add_task(async function visitDifferentOriginsRepeatedly() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example1.com/" },
+ { uri: "http://example1.com/" },
+ { uri: "http://example1.com/" },
+ { uri: "http://example2.com/" },
+ { uri: "http://example2.com/" },
+ { uri: "http://example3.com/" },
+ ]);
+ await checkDB([
+ ["http://", "example1.com", ["http://example1.com/"]],
+ ["http://", "example2.com", ["http://example2.com/"]],
+ ["http://", "example3.com", ["http://example3.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/");
+ await checkDB([
+ ["http://", "example2.com", ["http://example2.com/"]],
+ ["http://", "example3.com", ["http://example3.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/");
+ await checkDB([["http://", "example3.com", ["http://example3.com/"]]]);
+ await PlacesUtils.history.remove("http://example3.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Visiting URLs, some with the same and some with different origins, should
+// update moz_origins.
+add_task(async function visitDifferentOriginsDifferentURLs() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example1.com/1" },
+ { uri: "http://example1.com/2" },
+ { uri: "http://example1.com/3" },
+ { uri: "http://example2.com/1" },
+ { uri: "http://example2.com/2" },
+ { uri: "http://example3.com/1" },
+ ]);
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ [
+ "http://example1.com/1",
+ "http://example1.com/2",
+ "http://example1.com/3",
+ ],
+ ],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/1");
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ ["http://example1.com/2", "http://example1.com/3"],
+ ],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/2");
+ await checkDB([
+ ["http://", "example1.com", ["http://example1.com/3"]],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/3");
+ await checkDB([
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/1");
+ await checkDB([
+ ["http://", "example2.com", ["http://example2.com/2"]],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/2");
+ await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]);
+ await PlacesUtils.history.remove("http://example3.com/1");
+ await checkDB([]);
+});
+
+// Same as previous, but visits are added sequentially.
+add_task(async function visitDifferentOriginsDifferentURLsSequential() {
+ await PlacesTestUtils.addVisits([{ uri: "http://example1.com/1" }]);
+ await checkDB([["http://", "example1.com", ["http://example1.com/1"]]]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example1.com/2" }]);
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ ["http://example1.com/1", "http://example1.com/2"],
+ ],
+ ]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example1.com/3" }]);
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ [
+ "http://example1.com/1",
+ "http://example1.com/2",
+ "http://example1.com/3",
+ ],
+ ],
+ ]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example2.com/1" }]);
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ [
+ "http://example1.com/1",
+ "http://example1.com/2",
+ "http://example1.com/3",
+ ],
+ ],
+ ["http://", "example2.com", ["http://example2.com/1"]],
+ ]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example2.com/2" }]);
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ [
+ "http://example1.com/1",
+ "http://example1.com/2",
+ "http://example1.com/3",
+ ],
+ ],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ]);
+ await PlacesTestUtils.addVisits([{ uri: "http://example3.com/1" }]);
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ [
+ "http://example1.com/1",
+ "http://example1.com/2",
+ "http://example1.com/3",
+ ],
+ ],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/1");
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ ["http://example1.com/2", "http://example1.com/3"],
+ ],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/2");
+ await checkDB([
+ ["http://", "example1.com", ["http://example1.com/3"]],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/3");
+ await checkDB([
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/1");
+ await checkDB([
+ ["http://", "example2.com", ["http://example2.com/2"]],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/2");
+ await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]);
+ await PlacesUtils.history.remove("http://example3.com/1");
+ await checkDB([]);
+});
+
+// Repeatedly visiting URLs, some with the same and some with different origins,
+// should update moz_origins (with the correct frecencies).
+add_task(async function visitDifferentOriginsDifferentURLsRepeatedly() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example1.com/1" },
+ { uri: "http://example1.com/1" },
+ { uri: "http://example1.com/1" },
+ { uri: "http://example1.com/2" },
+ { uri: "http://example1.com/2" },
+ { uri: "http://example1.com/3" },
+ { uri: "http://example2.com/1" },
+ { uri: "http://example2.com/1" },
+ { uri: "http://example2.com/1" },
+ { uri: "http://example2.com/2" },
+ { uri: "http://example2.com/2" },
+ { uri: "http://example3.com/1" },
+ { uri: "http://example3.com/1" },
+ ]);
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ [
+ "http://example1.com/1",
+ "http://example1.com/2",
+ "http://example1.com/3",
+ ],
+ ],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/1");
+ await checkDB([
+ [
+ "http://",
+ "example1.com",
+ ["http://example1.com/2", "http://example1.com/3"],
+ ],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/2");
+ await checkDB([
+ ["http://", "example1.com", ["http://example1.com/3"]],
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example1.com/3");
+ await checkDB([
+ [
+ "http://",
+ "example2.com",
+ ["http://example2.com/1", "http://example2.com/2"],
+ ],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/1");
+ await checkDB([
+ ["http://", "example2.com", ["http://example2.com/2"]],
+ ["http://", "example3.com", ["http://example3.com/1"]],
+ ]);
+ await PlacesUtils.history.remove("http://example2.com/2");
+ await checkDB([["http://", "example3.com", ["http://example3.com/1"]]]);
+ await PlacesUtils.history.remove("http://example3.com/1");
+ await checkDB([]);
+});
+
+// Makes sure URIs with the same TLD but different www subdomains are recognized
+// as different origins. Makes sure removing one doesn't remove the others.
+add_task(async function www1() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://www.example.com/" },
+ { uri: "http://www.www.example.com/" },
+ ]);
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/"]],
+ ["http://", "www.example.com", ["http://www.example.com/"]],
+ ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([
+ ["http://", "www.example.com", ["http://www.example.com/"]],
+ ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://www.example.com/");
+ await checkDB([
+ ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://www.www.example.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Same as www1, but removes URIs in a different order.
+add_task(async function www2() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://www.example.com/" },
+ { uri: "http://www.www.example.com/" },
+ ]);
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/"]],
+ ["http://", "www.example.com", ["http://www.example.com/"]],
+ ["http://", "www.www.example.com", ["http://www.www.example.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://www.www.example.com/");
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/"]],
+ ["http://", "www.example.com", ["http://www.example.com/"]],
+ ]);
+ await PlacesUtils.history.remove("http://www.example.com/");
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Makes sure removing an origin without a port doesn't remove the same host
+// with a port.
+add_task(async function ports1() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://example.com:8888/" },
+ ]);
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/"]],
+ ["http://", "example.com:8888", ["http://example.com:8888/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([
+ ["http://", "example.com:8888", ["http://example.com:8888/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example.com:8888/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Makes sure removing an origin with a port doesn't remove the same host
+// without a port.
+add_task(async function ports2() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://example.com:8888/" },
+ ]);
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/"]],
+ ["http://", "example.com:8888", ["http://example.com:8888/"]],
+ ]);
+ await PlacesUtils.history.remove("http://example.com:8888/");
+ await checkDB([["http://", "example.com", ["http://example.com/"]]]);
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Makes sure multiple URIs with the same origin don't create duplicate origins.
+add_task(async function duplicates() {
+ await PlacesTestUtils.addVisits([
+ { uri: "http://example.com/" },
+ { uri: "http://www.example.com/" },
+ { uri: "http://www.www.example.com/" },
+ { uri: "https://example.com/" },
+ { uri: "ftp://example.com/" },
+ { uri: "foo://example.com/" },
+ { uri: "bar:example.com/" },
+ { uri: "http://example.com:8888/" },
+
+ { uri: "http://example.com/dupe" },
+ { uri: "http://www.example.com/dupe" },
+ { uri: "http://www.www.example.com/dupe" },
+ { uri: "https://example.com/dupe" },
+ { uri: "ftp://example.com/dupe" },
+ { uri: "foo://example.com/dupe" },
+ { uri: "bar:example.com/dupe" },
+ { uri: "http://example.com:8888/dupe" },
+ ]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/", "http://example.com/dupe"],
+ ],
+ [
+ "http://",
+ "www.example.com",
+ ["http://www.example.com/", "http://www.example.com/dupe"],
+ ],
+ [
+ "http://",
+ "www.www.example.com",
+ ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
+ ],
+ [
+ "https://",
+ "example.com",
+ ["https://example.com/", "https://example.com/dupe"],
+ ],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("http://example.com/");
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/dupe"]],
+ [
+ "http://",
+ "www.example.com",
+ ["http://www.example.com/", "http://www.example.com/dupe"],
+ ],
+ [
+ "http://",
+ "www.www.example.com",
+ ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
+ ],
+ [
+ "https://",
+ "example.com",
+ ["https://example.com/", "https://example.com/dupe"],
+ ],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://example.com/dupe");
+ await checkDB([
+ [
+ "http://",
+ "www.example.com",
+ ["http://www.example.com/", "http://www.example.com/dupe"],
+ ],
+ [
+ "http://",
+ "www.www.example.com",
+ ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
+ ],
+ [
+ "https://",
+ "example.com",
+ ["https://example.com/", "https://example.com/dupe"],
+ ],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("http://www.example.com/");
+ await checkDB([
+ ["http://", "www.example.com", ["http://www.example.com/dupe"]],
+ [
+ "http://",
+ "www.www.example.com",
+ ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
+ ],
+ [
+ "https://",
+ "example.com",
+ ["https://example.com/", "https://example.com/dupe"],
+ ],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://www.example.com/dupe");
+ await checkDB([
+ [
+ "http://",
+ "www.www.example.com",
+ ["http://www.www.example.com/", "http://www.www.example.com/dupe"],
+ ],
+ [
+ "https://",
+ "example.com",
+ ["https://example.com/", "https://example.com/dupe"],
+ ],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("http://www.www.example.com/");
+ await checkDB([
+ ["http://", "www.www.example.com", ["http://www.www.example.com/dupe"]],
+ [
+ "https://",
+ "example.com",
+ ["https://example.com/", "https://example.com/dupe"],
+ ],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("http://www.www.example.com/dupe");
+ await checkDB([
+ [
+ "https://",
+ "example.com",
+ ["https://example.com/", "https://example.com/dupe"],
+ ],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("https://example.com/");
+ await checkDB([
+ ["https://", "example.com", ["https://example.com/dupe"]],
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("https://example.com/dupe");
+ await checkDB([
+ ["ftp://", "example.com", ["ftp://example.com/", "ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("ftp://example.com/");
+ await checkDB([
+ ["ftp://", "example.com", ["ftp://example.com/dupe"]],
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("ftp://example.com/dupe");
+ await checkDB([
+ ["foo://", "example.com", ["foo://example.com/", "foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("foo://example.com/");
+ await checkDB([
+ ["foo://", "example.com", ["foo://example.com/dupe"]],
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("foo://example.com/dupe");
+ await checkDB([
+ ["bar:", "example.com", ["bar:example.com/", "bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("bar:example.com/");
+ await checkDB([
+ ["bar:", "example.com", ["bar:example.com/dupe"]],
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+ await PlacesUtils.history.remove("bar:example.com/dupe");
+ await checkDB([
+ [
+ "http://",
+ "example.com:8888",
+ ["http://example.com:8888/", "http://example.com:8888/dupe"],
+ ],
+ ]);
+
+ await PlacesUtils.history.remove("http://example.com:8888/");
+ await checkDB([
+ ["http://", "example.com:8888", ["http://example.com:8888/dupe"]],
+ ]);
+ await PlacesUtils.history.remove("http://example.com:8888/dupe");
+ await checkDB([]);
+
+ await cleanUp();
+});
+
+// Makes sure adding and removing bookmarks creates origins.
+add_task(async function addRemoveBookmarks() {
+ let bookmarks = [];
+ let urls = ["http://example.com/", "http://www.example.com/"];
+ for (let url of urls) {
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ })
+ );
+ }
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/"]],
+ ["http://", "www.example.com", ["http://www.example.com/"]],
+ ]);
+ await PlacesUtils.bookmarks.remove(bookmarks[0]);
+ await PlacesUtils.history.clear();
+ await checkDB([["http://", "www.example.com", ["http://www.example.com/"]]]);
+ await PlacesUtils.bookmarks.remove(bookmarks[1]);
+ await PlacesUtils.history.clear();
+ await checkDB([]);
+ await cleanUp();
+});
+
+// Makes sure changing bookmarks also changes the corresponding origins.
+add_task(async function changeBookmarks() {
+ let bookmarks = [];
+ let urls = ["http://example.com/", "http://www.example.com/"];
+ for (let url of urls) {
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ url,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ })
+ );
+ }
+ await checkDB([
+ ["http://", "example.com", ["http://example.com/"]],
+ ["http://", "www.example.com", ["http://www.example.com/"]],
+ ]);
+ await PlacesUtils.bookmarks.update({
+ url: "http://www.example.com/",
+ guid: bookmarks[0].guid,
+ });
+ await PlacesUtils.history.clear();
+ await checkDB([["http://", "www.example.com", ["http://www.example.com/"]]]);
+ await cleanUp();
+});
+
+// A slightly more complex test to make sure origin frecency stats are updated
+// when visits and bookmarks are added and removed.
+add_task(async function moreOriginFrecencyStats() {
+ await checkDB([]);
+
+ // Add a URL 0 visit.
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/0" }]);
+ await checkDB([["http://", "example.com", ["http://example.com/0"]]]);
+
+ // Add a URL 1 visit.
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/1" }]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/0", "http://example.com/1"],
+ ],
+ ]);
+
+ // Add a URL 2 visit.
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/2" }]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/0", "http://example.com/1", "http://example.com/2"],
+ ],
+ ]);
+
+ // Add another URL 2 visit.
+ await PlacesTestUtils.addVisits([{ uri: "http://example.com/2" }]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/0", "http://example.com/1", "http://example.com/2"],
+ ],
+ ]);
+
+ // Remove URL 2's visits.
+ await PlacesUtils.history.remove(["http://example.com/2"]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/0", "http://example.com/1"],
+ ],
+ ]);
+
+ // Bookmark URL 1.
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "A bookmark",
+ url: NetUtil.newURI("http://example.com/1"),
+ });
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/0", "http://example.com/1"],
+ ],
+ ]);
+
+ // Remove URL 1's visit.
+ await PlacesUtils.history.remove(["http://example.com/1"]);
+ await checkDB([
+ [
+ "http://",
+ "example.com",
+ ["http://example.com/0", "http://example.com/1"],
+ ],
+ ]);
+
+ // Remove URL 1's bookmark. Also need to call history.remove() again to
+ // remove the URL from moz_places. Otherwise it sticks around and keeps
+ // contributing to the frecency stats.
+ await PlacesUtils.bookmarks.remove(bookmark);
+ await PlacesUtils.history.remove("http://example.com/1");
+ await checkDB([["http://", "example.com", ["http://example.com/0"]]]);
+
+ // Remove URL 0.
+ await PlacesUtils.history.remove(["http://example.com/0"]);
+ await checkDB([]);
+
+ await cleanUp();
+});
+
+/**
+ * Returns the expected frecency of the origin of the given URLs, i.e., the sum
+ * of their frecencies. Each URL is expected to have the same origin.
+ *
+ * @param urls
+ * An array of URL strings.
+ * @return The expected origin frecency.
+ */
+async function expectedOriginFrecency(urls) {
+ let value = 0;
+ for (let url of urls) {
+ let v = Math.max(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", { url }),
+ 0
+ );
+ value += v;
+ }
+ return value;
+}
+
+/**
+ * Asserts that the moz_origins table and the origin frecency stats are correct.
+ *
+ * @param expectedOrigins
+ * An array of expected origins. Each origin in the array is itself an
+ * array that looks like this:
+ * [prefix, host, [url1, url2, ..., urln]]
+ * The element at index 2 is an array of all the URLs with the origin.
+ * If you don't care about checking frecencies and origin frecency stats,
+ * this element can be `undefined`.
+ */
+async function checkDB(expectedOrigins) {
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`
+ SELECT prefix, host, frecency
+ FROM moz_origins
+ ORDER BY id ASC
+ `);
+ let checkFrecencies =
+ !expectedOrigins.length || expectedOrigins[0][2] !== undefined;
+ let actualOrigins = rows.map(row => {
+ let o = [];
+ for (let c = 0; c < (checkFrecencies ? 3 : 2); c++) {
+ o.push(row.getResultByIndex(c));
+ }
+ return o;
+ });
+ let expected = [];
+ for (let origin of expectedOrigins) {
+ expected.push(
+ origin
+ .slice(0, 2)
+ .concat(checkFrecencies ? await expectedOriginFrecency(origin[2]) : [])
+ );
+ }
+ Assert.deepEqual(actualOrigins, expected);
+ if (checkFrecencies) {
+ await checkStats(expected.map(o => o[2]).filter(o => o > 0));
+ }
+}
+
+/**
+ * Asserts that the origin frecency stats are correct.
+ *
+ * @param expectedOriginFrecencies
+ * An array of expected origin frecencies.
+ */
+async function checkStats(expectedOriginFrecencies) {
+ let stats = await promiseStats();
+ Assert.equal(stats.count, expectedOriginFrecencies.length);
+ Assert.equal(
+ stats.sum,
+ expectedOriginFrecencies.reduce((sum, f) => sum + f, 0)
+ );
+ Assert.equal(
+ stats.squares,
+ expectedOriginFrecencies.reduce((squares, f) => squares + f * f, 0)
+ );
+}
+
+/**
+ * Returns the origin frecency stats.
+ *
+ * @return An object: { count, sum, squares }
+ */
+async function promiseStats() {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`
+ SELECT
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_count'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum'), 0),
+ IFNULL((SELECT value FROM moz_meta WHERE key = 'origin_frecency_sum_of_squares'), 0)
+ `);
+ return {
+ count: rows[0].getResultByIndex(0),
+ sum: rows[0].getResultByIndex(1),
+ squares: rows[0].getResultByIndex(2),
+ };
+}
+
+async function cleanUp() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/toolkit/components/places/tests/unit/test_origins_parsing.js b/toolkit/components/places/tests/unit/test_origins_parsing.js
new file mode 100644
index 0000000000..35ba8bdd0d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_origins_parsing.js
@@ -0,0 +1,104 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This test is a companion to test_origins.js. It adds many URLs to the
+// database and makes sure that their prefixes and hosts are correctly parsed.
+// This test can take a while to run, which is why it's split out from
+// test_origins.js.
+
+"use strict";
+
+add_task(async function parsing() {
+ let prefixes = ["http://", "https://", "ftp://", "foo://", "bar:"];
+
+ let userinfos = ["", "user:pass@", "user:pass:word@", "user:@"];
+
+ let ports = ["", ":8888"];
+
+ let paths = [
+ "",
+
+ "/",
+ "/1",
+ "/1/2",
+
+ "?",
+ "?1",
+ "#",
+ "#1",
+
+ "/?",
+ "/1?",
+ "/?1",
+ "/1?2",
+
+ "/#",
+ "/1#",
+ "/#1",
+ "/1#2",
+
+ "/?#",
+ "/1?#",
+ "/?1#",
+ "/?#1",
+ "/1?2#",
+ "/1?#2",
+ "/?1#2",
+ ];
+
+ for (let userinfo of userinfos) {
+ for (let port of ports) {
+ for (let path of paths) {
+ info(`Testing userinfo='${userinfo}' port='${port}' path='${path}'`);
+ let expectedOrigins = prefixes.map(prefix => [
+ prefix,
+ "example.com" + port,
+ ]);
+ let uris = expectedOrigins.map(
+ ([prefix, hostPort]) => prefix + userinfo + hostPort + path
+ );
+
+ await PlacesTestUtils.addVisits(uris.map(uri => ({ uri })));
+ await checkDB(expectedOrigins);
+
+ // Remove each URI, one at a time, and make sure the remaining origins
+ // in the database are correct.
+ for (let i = 0; i < uris.length; i++) {
+ await PlacesUtils.history.remove(uris[i]);
+ await checkDB(expectedOrigins.slice(i + 1, expectedOrigins.length));
+ }
+ await cleanUp();
+ }
+ }
+ }
+ await checkDB([]);
+});
+
+/**
+ * Asserts that the moz_origins table is correct.
+ *
+ * @param expectedOrigins
+ * An array of expected origins. Each origin in the array is itself an
+ * array that looks like this: [prefix, host]
+ */
+async function checkDB(expectedOrigins) {
+ let db = await PlacesUtils.promiseDBConnection();
+ let rows = await db.execute(`
+ SELECT prefix, host
+ FROM moz_origins
+ ORDER BY id ASC
+ `);
+ let actualOrigins = rows.map(row => {
+ let o = [];
+ for (let c = 0; c < 2; c++) {
+ o.push(row.getResultByIndex(c));
+ }
+ return o;
+ });
+ Assert.deepEqual(actualOrigins, expectedOrigins);
+}
+
+async function cleanUp() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+}
diff --git a/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js
new file mode 100644
index 0000000000..e1fa64a88c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js
@@ -0,0 +1,256 @@
+/* -*- 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/. */
+
+const histsvc = PlacesUtils.history;
+
+add_task(async function test_addBookmarksAndCheckGuids() {
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ url: "http://test1.com/",
+ title: "1 title",
+ },
+ {
+ url: "http://test2.com/",
+ title: "2 title",
+ },
+ {
+ url: "http://test3.com/",
+ title: "3 title",
+ },
+ {
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ },
+ {
+ title: "test folder 2",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ ],
+ },
+ ],
+ });
+
+ let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root;
+ Assert.equal(root.childCount, 5);
+
+ // check bookmark guids
+ let bookmarkGuidZero = root.getChild(0).bookmarkGuid;
+ Assert.equal(bookmarkGuidZero.length, 12);
+ // bookmarks have bookmark guids
+ Assert.equal(root.getChild(1).bookmarkGuid.length, 12);
+ Assert.equal(root.getChild(2).bookmarkGuid.length, 12);
+ // separator has bookmark guid
+ Assert.equal(root.getChild(3).bookmarkGuid.length, 12);
+ // folder has bookmark guid
+ Assert.equal(root.getChild(4).bookmarkGuid.length, 12);
+ // all bookmark guids are different.
+ Assert.notEqual(bookmarkGuidZero, root.getChild(1).bookmarkGuid);
+ Assert.notEqual(root.getChild(1).bookmarkGuid, root.getChild(2).bookmarkGuid);
+ Assert.notEqual(root.getChild(2).bookmarkGuid, root.getChild(3).bookmarkGuid);
+ Assert.notEqual(root.getChild(3).bookmarkGuid, root.getChild(4).bookmarkGuid);
+
+ // check page guids
+ let pageGuidZero = root.getChild(0).pageGuid;
+ Assert.equal(pageGuidZero.length, 12);
+ // bookmarks have page guids
+ Assert.equal(root.getChild(1).pageGuid.length, 12);
+ Assert.equal(root.getChild(2).pageGuid.length, 12);
+ // folder and separator don't have page guids
+ Assert.equal(root.getChild(3).pageGuid, "");
+ Assert.equal(root.getChild(4).pageGuid, "");
+
+ Assert.notEqual(pageGuidZero, root.getChild(1).pageGuid);
+ Assert.notEqual(root.getChild(1).pageGuid, root.getChild(2).pageGuid);
+
+ root.containerOpen = false;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_updateBookmarksAndCheckGuids() {
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ url: "http://test1.com/",
+ title: "1 title",
+ },
+ {
+ title: "test folder 2",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ ],
+ },
+ ],
+ });
+
+ let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root;
+ Assert.equal(root.childCount, 2);
+
+ // ensure the bookmark and page guids remain the same after modifing other property.
+ let bookmarkGuidZero = root.getChild(0).bookmarkGuid;
+ let pageGuidZero = root.getChild(0).pageGuid;
+ await PlacesUtils.bookmarks.update({
+ guid: bookmarks[1].guid,
+ title: "1 title mod",
+ });
+ Assert.equal(root.getChild(0).title, "1 title mod");
+ Assert.equal(root.getChild(0).bookmarkGuid, bookmarkGuidZero);
+ Assert.equal(root.getChild(0).pageGuid, pageGuidZero);
+
+ let bookmarkGuidOne = root.getChild(1).bookmarkGuid;
+ let pageGuidOne = root.getChild(1).pageGuid;
+
+ await PlacesUtils.bookmarks.update({
+ guid: bookmarks[2].guid,
+ title: "test foolder 234",
+ });
+ Assert.equal(root.getChild(1).title, "test foolder 234");
+ Assert.equal(root.getChild(1).bookmarkGuid, bookmarkGuidOne);
+ Assert.equal(root.getChild(1).pageGuid, pageGuidOne);
+
+ root.containerOpen = false;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_addVisitAndCheckGuid() {
+ // add a visit and test page guid and non-existing bookmark guids.
+ let sourceURI = uri("http://test4.com/");
+ await PlacesTestUtils.addVisits({ uri: sourceURI });
+ Assert.equal(await PlacesUtils.bookmarks.fetch({ url: sourceURI }, null));
+
+ let options = histsvc.getNewQueryOptions();
+ let query = histsvc.getNewQuery();
+ query.uri = sourceURI;
+ let root = histsvc.executeQuery(query, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+
+ do_check_valid_places_guid(root.getChild(0).pageGuid);
+ Assert.equal(root.getChild(0).bookmarkGuid, "");
+ root.containerOpen = false;
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_addItemsWithInvalidGUIDsFails() {
+ const INVALID_GUID = "XYZ";
+ try {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ guid: INVALID_GUID,
+ title: "XYZ folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ do_throw("Adding a folder with an invalid guid should fail");
+ } catch (ex) {}
+
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ try {
+ PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ guid: INVALID_GUID,
+ title: "title",
+ url: "http://test.tld",
+ });
+ do_throw("Adding a bookmark with an invalid guid should fail");
+ } catch (ex) {}
+
+ try {
+ PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ guid: INVALID_GUID,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ });
+ do_throw("Adding a separator with an invalid guid should fail");
+ } catch (ex) {}
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_addItemsWithGUIDs() {
+ const FOLDER_GUID = "FOLDER--GUID";
+ const BOOKMARK_GUID = "BM------GUID";
+ const SEPARATOR_GUID = "SEP-----GUID";
+
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ guid: FOLDER_GUID,
+ children: [
+ {
+ url: "http://test1.com",
+ title: "1 title",
+ guid: BOOKMARK_GUID,
+ },
+ {
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ guid: SEPARATOR_GUID,
+ },
+ ],
+ },
+ ],
+ });
+
+ let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root;
+ Assert.equal(root.childCount, 2);
+ Assert.equal(root.bookmarkGuid, FOLDER_GUID);
+ Assert.equal(root.getChild(0).bookmarkGuid, BOOKMARK_GUID);
+ Assert.equal(root.getChild(1).bookmarkGuid, SEPARATOR_GUID);
+
+ root.containerOpen = false;
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_emptyGUIDFails() {
+ try {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ guid: "",
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ do_throw("Adding a folder with an empty guid should fail");
+ } catch (ex) {}
+});
+
+add_task(async function test_usingSameGUIDFails() {
+ const GUID = "XYZXYZXYZXYZ";
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ guid: GUID,
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ try {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ guid: GUID,
+ title: "test folder 2",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ do_throw("Using the same guid twice should fail");
+ } catch (ex) {}
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_placeURIs.js b/toolkit/components/places/tests/unit/test_placeURIs.js
new file mode 100644
index 0000000000..093dc89f68
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_placeURIs.js
@@ -0,0 +1,18 @@
+/* -*- 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/. */
+
+function run_test() {
+ // TODO: Improve testing coverage for QueryToQueryString and QueryStringToQuery
+
+ // Bug 376798
+ let query = PlacesUtils.history.getNewQuery();
+ let options = PlacesUtils.history.getNewQueryOptions();
+ query.setParents([PlacesUtils.bookmarks.rootGuid]);
+ Assert.equal(
+ PlacesUtils.history.queryToQueryString(query, options),
+ `place:parent=${PlacesUtils.bookmarks.rootGuid}`
+ );
+}
diff --git a/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
new file mode 100644
index 0000000000..e42bf3b2d8
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
@@ -0,0 +1,282 @@
+/* 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/. */
+
+async function check_has_child(aParentGuid, aChildGuid) {
+ let parentTree = await PlacesUtils.promiseBookmarksTree(aParentGuid);
+ Assert.ok("children" in parentTree);
+ Assert.notEqual(
+ parentTree.children.find(e => e.guid == aChildGuid),
+ null
+ );
+}
+
+async function compareToNode(aItem, aNode, aIsRootItem, aExcludedGuids = []) {
+ // itemId==-1 indicates a non-bookmark node, which is unexpected.
+ Assert.notEqual(aNode.itemId, -1);
+
+ function check_unset(...aProps) {
+ for (let p of aProps) {
+ if (p in aItem) {
+ Assert.ok(false, `Unexpected property ${p} with value ${aItem[p]}`);
+ }
+ }
+ }
+
+ function compare_prop(aItemProp, aNodeProp = aItemProp, aOptional = false) {
+ if (aOptional && aNode[aNodeProp] === null) {
+ check_unset(aItemProp);
+ } else {
+ Assert.strictEqual(aItem[aItemProp], aNode[aNodeProp]);
+ }
+ }
+
+ // Bug 1013053 - bookmarkIndex is unavailable for the query's root
+ if (aNode.bookmarkIndex == -1) {
+ let bookmark = await PlacesUtils.bookmarks.fetch(aNode.bookmarkGuid);
+ Assert.strictEqual(aItem.index, bookmark.index);
+ } else {
+ compare_prop("index", "bookmarkIndex");
+ }
+
+ compare_prop("dateAdded");
+ compare_prop("lastModified");
+
+ if (aIsRootItem && aNode.bookmarkGuid != PlacesUtils.bookmarks.rootGuid) {
+ Assert.ok("parentGuid" in aItem);
+ await check_has_child(aItem.parentGuid, aItem.guid);
+ } else {
+ check_unset("parentGuid");
+ }
+
+ const BOOKMARK_ONLY_PROPS = ["uri", "iconUri", "tags", "charset", "keyword"];
+ const FOLDER_ONLY_PROPS = ["children", "root"];
+
+ let nodesCount = 1;
+
+ switch (aNode.type) {
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER:
+ Assert.equal(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
+ Assert.equal(aItem.typeCode, PlacesUtils.bookmarks.TYPE_FOLDER);
+ compare_prop("title", "title", true);
+ check_unset(...BOOKMARK_ONLY_PROPS);
+
+ let expectedChildrenNodes = [];
+
+ PlacesUtils.asContainer(aNode);
+ if (!aNode.containerOpen) {
+ aNode.containerOpen = true;
+ }
+
+ for (let i = 0; i < aNode.childCount; i++) {
+ let childNode = aNode.getChild(i);
+ if (
+ childNode.itemId == PlacesUtils.tagsFolderId ||
+ aExcludedGuids.includes(childNode.bookmarkGuid)
+ ) {
+ continue;
+ }
+ expectedChildrenNodes.push(childNode);
+ }
+
+ if (expectedChildrenNodes.length) {
+ Assert.ok(Array.isArray(aItem.children));
+ Assert.equal(aItem.children.length, expectedChildrenNodes.length);
+ for (let i = 0; i < aItem.children.length; i++) {
+ nodesCount += await compareToNode(
+ aItem.children[i],
+ expectedChildrenNodes[i],
+ false,
+ aExcludedGuids
+ );
+ }
+ } else {
+ check_unset("children");
+ }
+
+ let rootName = mapItemGuidToInternalRootName(aItem.guid);
+ if (rootName) {
+ Assert.equal(aItem.root, rootName);
+ } else {
+ check_unset("root");
+ }
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
+ Assert.equal(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
+ Assert.equal(aItem.typeCode, PlacesUtils.bookmarks.TYPE_SEPARATOR);
+ check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS);
+ break;
+ default:
+ Assert.equal(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE);
+ Assert.equal(aItem.typeCode, PlacesUtils.bookmarks.TYPE_BOOKMARK);
+ compare_prop("uri");
+ // node.tags's format is "a, b" whilst promiseBoookmarksTree is "a,b"
+ if (aNode.tags === null) {
+ check_unset("tags");
+ } else {
+ Assert.equal(aItem.tags, aNode.tags.replace(/, /g, ","));
+ }
+
+ if (aNode.icon) {
+ try {
+ await compareFavicons(aNode.icon, aItem.iconUri);
+ } catch (ex) {
+ info(ex);
+ todo_check_true(false);
+ }
+ } else {
+ check_unset(aItem.iconUri);
+ }
+
+ check_unset(...FOLDER_ONLY_PROPS);
+
+ let pageInfo = await PlacesUtils.history.fetch(aNode.uri, {
+ includeAnnotations: true,
+ });
+ let expectedCharset = pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO);
+ if (expectedCharset) {
+ Assert.equal(aItem.charset, expectedCharset);
+ } else {
+ check_unset("charset");
+ }
+
+ let entry = await PlacesUtils.keywords.fetch({ url: aNode.uri });
+ if (entry) {
+ Assert.equal(aItem.keyword, entry.keyword);
+ } else {
+ check_unset("keyword");
+ }
+
+ if ("title" in aItem) {
+ compare_prop("title");
+ } else {
+ Assert.equal(null, aNode.title);
+ }
+ }
+
+ if (aIsRootItem) {
+ Assert.strictEqual(aItem.itemsCount, nodesCount);
+ }
+
+ return nodesCount;
+}
+
+var itemsCount = 0;
+async function new_bookmark(aInfo) {
+ ++itemsCount;
+ if (!("url" in aInfo)) {
+ aInfo.url = uri("http://test.item.y" + itemsCount);
+ }
+
+ if (!("title" in aInfo)) {
+ aInfo.title = "Test Item (bookmark) " + itemsCount;
+ }
+
+ await PlacesTransactions.NewBookmark(aInfo).transact();
+}
+
+function new_folder(aInfo) {
+ if (!("title" in aInfo)) {
+ aInfo.title = "Test Item (folder) " + itemsCount;
+ }
+ return PlacesTransactions.NewFolder(aInfo).transact();
+}
+
+// Walks a result nodes tree and test promiseBookmarksTree for each node.
+// DO NOT COPY THIS LOGIC: It is done here to accomplish a more comprehensive
+// test of the API (the entire hierarchy data is available in the very test).
+async function test_promiseBookmarksTreeForEachNode(
+ aNode,
+ aOptions,
+ aExcludedGuids
+) {
+ Assert.ok(aNode.bookmarkGuid && !!aNode.bookmarkGuid.length);
+ let item = await PlacesUtils.promiseBookmarksTree(
+ aNode.bookmarkGuid,
+ aOptions
+ );
+ await compareToNode(item, aNode, true, aExcludedGuids);
+
+ if (!PlacesUtils.nodeIsContainer(aNode)) {
+ return item;
+ }
+
+ for (let i = 0; i < aNode.childCount; i++) {
+ let child = aNode.getChild(i);
+ if (child.itemId != PlacesUtils.tagsFolderId) {
+ await test_promiseBookmarksTreeForEachNode(
+ child,
+ { includeItemIds: true },
+ aExcludedGuids
+ );
+ }
+ }
+ return item;
+}
+
+async function test_promiseBookmarksTreeAgainstResult(
+ aItemGuid = PlacesUtils.bookmarks.rootGuid,
+ aOptions = { includeItemIds: true },
+ aExcludedGuids
+) {
+ let node = PlacesUtils.getFolderContents(aItemGuid).root;
+ return test_promiseBookmarksTreeForEachNode(node, aOptions, aExcludedGuids);
+}
+
+add_task(async function () {
+ // Add some bookmarks to cover various use cases.
+ await new_bookmark({ parentGuid: PlacesUtils.bookmarks.toolbarGuid });
+ await new_folder({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ annotations: [
+ { name: "TestAnnoA", value: "TestVal" },
+ { name: "TestAnnoB", value: 0 },
+ ],
+ });
+ let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid };
+ await PlacesTransactions.NewSeparator(sepInfo).transact();
+ let folderGuid = await new_folder({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ });
+ await new_bookmark({
+ title: null,
+ parentGuid: folderGuid,
+ keyword: "test_keyword",
+ tags: ["TestTagA", "TestTagB"],
+ annotations: [{ name: "TestAnnoA", value: "TestVal2" }],
+ });
+ let urlWithCharsetAndFavicon = uri("http://charset.and.favicon");
+ await new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon });
+ await PlacesUtils.history.update({
+ url: urlWithCharsetAndFavicon,
+ annotations: new Map([[PlacesUtils.CHARSET_ANNO, "UTF-16"]]),
+ });
+ await setFaviconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI);
+ // Test the default places root without specifying it.
+ await test_promiseBookmarksTreeAgainstResult();
+
+ // Do specify it
+ await test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid);
+
+ // Exclude the bookmarks menu.
+ // The calllback should be four times - once for the toolbar, once for
+ // the bookmark we inserted under, and once for the menu (and not
+ // at all for any of its descendants) and once for the unsorted bookmarks
+ // folder. However, promiseBookmarksTree is called multiple times, so
+ // rather than counting the calls, we count the number of unique items
+ // passed in.
+ let guidsPassedToExcludeCallback = new Set();
+ let placesRootWithoutTheMenu = await test_promiseBookmarksTreeAgainstResult(
+ PlacesUtils.bookmarks.rootGuid,
+ {
+ excludeItemsCallback: aItem => {
+ guidsPassedToExcludeCallback.add(aItem.guid);
+ return aItem.root == "bookmarksMenuFolder";
+ },
+ includeItemIds: true,
+ },
+ [PlacesUtils.bookmarks.menuGuid]
+ );
+ Assert.equal(guidsPassedToExcludeCallback.size, 5);
+ Assert.equal(placesRootWithoutTheMenu.children.length, 3);
+});
diff --git a/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js
new file mode 100644
index 0000000000..d8830c5686
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js
@@ -0,0 +1,39 @@
+/* -*- 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 () {
+ let uri1 = uri("http://foo.tld/");
+ let uri2 = uri("https://bar.tld/");
+
+ await PlacesTestUtils.addVisits([
+ { uri: uri1, title: "foo title" },
+ { uri: uri2, title: "bar title" },
+ ]);
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: uri1,
+ title: null,
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: uri2,
+ title: null,
+ });
+
+ PlacesUtils.tagging.tagURI(uri1, ["tag 1"]);
+ PlacesUtils.tagging.tagURI(uri2, ["tag 2"]);
+});
+
+add_task(async function testTagQuery() {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ query.tags = ["tag 1"];
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, 1);
+ Assert.equal(root.getChild(0).title, "");
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_result_sort.js b/toolkit/components/places/tests/unit/test_result_sort.js
new file mode 100644
index 0000000000..840b40c339
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_result_sort.js
@@ -0,0 +1,112 @@
+/* -*- 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/. */
+
+const NHQO = Ci.nsINavHistoryQueryOptions;
+
+add_task(async function test() {
+ const uri1 = "http://foo.tld/a";
+ const uri2 = "http://foo.tld/b";
+
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "Result-sort functionality tests root",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "b",
+ url: uri1,
+ },
+ {
+ title: "a",
+ url: uri2,
+ },
+ {
+ // url of the first child, title of second
+ title: "a",
+ url: uri1,
+ },
+ ],
+ },
+ ],
+ });
+
+ let guid1 = bookmarks[1].guid;
+ let guid2 = bookmarks[2].guid;
+ let guid3 = bookmarks[3].guid;
+
+ // query with natural order
+ let result = PlacesUtils.getFolderContents(bookmarks[0].guid);
+ let root = result.root;
+
+ Assert.equal(root.childCount, 3);
+
+ function checkOrder(a, b, c) {
+ Assert.equal(root.getChild(0).bookmarkGuid, a);
+ Assert.equal(root.getChild(1).bookmarkGuid, b);
+ Assert.equal(root.getChild(2).bookmarkGuid, c);
+ }
+
+ // natural order
+ info("Natural order");
+ checkOrder(guid1, guid2, guid3);
+
+ // title: guid3 should precede guid2 since we fall-back to URI-based sorting
+ info("Sort by title asc");
+ result.sortingMode = NHQO.SORT_BY_TITLE_ASCENDING;
+ checkOrder(guid3, guid2, guid1);
+
+ // In reverse
+ info("Sort by title desc");
+ result.sortingMode = NHQO.SORT_BY_TITLE_DESCENDING;
+ checkOrder(guid1, guid2, guid3);
+
+ // uri sort: guid1 should precede guid3 since we fall-back to natural order
+ info("Sort by uri asc");
+ result.sortingMode = NHQO.SORT_BY_URI_ASCENDING;
+ checkOrder(guid1, guid3, guid2);
+
+ // test live update
+ info("Change bookmark uri liveupdate");
+ await PlacesUtils.bookmarks.update({
+ guid: guid1,
+ url: uri2,
+ });
+ checkOrder(guid3, guid1, guid2);
+ await PlacesUtils.bookmarks.update({
+ guid: guid1,
+ url: uri1,
+ });
+ checkOrder(guid1, guid3, guid2);
+
+ // XXXtodo: test history sortings (visit count, visit date)
+ // XXXtodo: test different item types once folderId and bookmarkId are merged.
+
+ // XXXtodo: test dateAdded sort
+ // XXXtodo: test lastModified sort
+
+ // Add a visit, then check frecency ordering.
+
+ await PlacesTestUtils.addVisits({ uri: uri2, transition: TRANSITION_TYPED });
+
+ info("Sort by frecency desc");
+ result.sortingMode = NHQO.SORT_BY_FRECENCY_DESCENDING;
+ for (let i = 0; i < root.childCount; ++i) {
+ print(root.getChild(i).uri + " " + root.getChild(i).title);
+ }
+ // For guid1 and guid3, since they have same frecency and no visits, fallback
+ // to sort by the newest bookmark.
+ checkOrder(guid2, guid3, guid1);
+ info("Sort by frecency asc");
+ result.sortingMode = NHQO.SORT_BY_FRECENCY_ASCENDING;
+ for (let i = 0; i < root.childCount; ++i) {
+ print(root.getChild(i).uri + " " + root.getChild(i).title);
+ }
+ checkOrder(guid1, guid3, guid2);
+
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js
new file mode 100644
index 0000000000..29963ad269
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js
@@ -0,0 +1,100 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+const { history } = PlacesUtils;
+
+add_task(async function test_addVisitCheckFields() {
+ let uri = NetUtil.newURI("http://test4.com/");
+ await PlacesTestUtils.addVisits([
+ { uri },
+ { uri, referrer: uri },
+ { uri, transition: history.TRANSITION_TYPED },
+ ]);
+
+ let options = history.getNewQueryOptions();
+ let query = history.getNewQuery();
+
+ query.uri = uri;
+
+ // Check RESULTS_AS_VISIT node.
+ options.resultType = options.RESULTS_AS_VISIT;
+
+ let root = history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ equal(root.childCount, 3);
+
+ let child = root.getChild(0);
+ equal(
+ child.visitType,
+ history.TRANSITION_LINK,
+ "Visit type should be TRANSITION_LINK"
+ );
+ equal(child.visitId, 1, "Visit ID should be 1");
+
+ child = root.getChild(1);
+ equal(
+ child.visitType,
+ history.TRANSITION_LINK,
+ "Visit type should be TRANSITION_LINK"
+ );
+ equal(child.visitId, 2, "Visit ID should be 2");
+
+ child = root.getChild(2);
+ equal(
+ child.visitType,
+ history.TRANSITION_TYPED,
+ "Visit type should be TRANSITION_TYPED"
+ );
+ equal(child.visitId, 3, "Visit ID should be 3");
+
+ root.containerOpen = false;
+
+ // Check RESULTS_AS_URI node.
+ options.resultType = options.RESULTS_AS_URI;
+
+ root = history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ equal(root.childCount, 1);
+
+ child = root.getChild(0);
+ equal(child.visitType, 0, "Visit type should be 0");
+ equal(child.visitId, -1, "Visit ID should be -1");
+
+ root.containerOpen = false;
+
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_bookmarkFields() {
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "test folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "test title",
+ url: "http://test4.com",
+ },
+ ],
+ },
+ ],
+ });
+
+ let root = PlacesUtils.getFolderContents(bookmarks[0].guid).root;
+ equal(root.childCount, 1);
+
+ equal(root.visitType, 0, "Visit type should be 0");
+ equal(root.visitId, -1, "Visit ID should be -1");
+
+ let child = root.getChild(0);
+ equal(child.visitType, 0, "Visit type should be 0");
+ equal(child.visitId, -1, "Visit ID should be -1");
+
+ root.containerOpen = false;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_sql_function_origin.js b/toolkit/components/places/tests/unit/test_sql_function_origin.js
new file mode 100644
index 0000000000..0314ff5040
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_sql_function_origin.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the origin-related SQL functions, which are:
+// * get_host_and_port
+// * get_prefix
+// * strip_prefix_and_userinfo
+
+// Tests actual URL strings.
+add_task(async function urls() {
+ let sets = [
+ ["http:"],
+ ["", "//"],
+ ["", "user@", "user:@", "user:pass@", "user:pass:word@"],
+ ["example.com"],
+ ["", ":8888"],
+ ["", "/", "/foo"],
+ ["", "?", "?bar"],
+ ["", "#", "#baz"],
+ ];
+ let db = await PlacesUtils.promiseDBConnection();
+ for (let parts of permute(sets)) {
+ let spec = parts.join("");
+ let funcs = {
+ get_prefix: parts.slice(0, 2).join(""),
+ get_host_and_port: parts.slice(3, 5).join(""),
+ strip_prefix_and_userinfo: parts.slice(3).join(""),
+ };
+ for (let [func, expectedValue] of Object.entries(funcs)) {
+ let rows = await db.execute(`
+ SELECT ${func}("${spec}");
+ `);
+ let value = rows[0].getString(0);
+ Assert.equal(value, expectedValue, `function=${func} spec="${spec}"`);
+ }
+ }
+});
+
+// Tests strings that aren't URLs.
+add_task(async function nonURLs() {
+ let db = await PlacesUtils.promiseDBConnection();
+
+ let value = (
+ await db.execute(`
+ SELECT get_prefix("hello");
+ `)
+ )[0].getString(0);
+ Assert.equal(value, "");
+
+ value = (
+ await db.execute(`
+ SELECT get_host_and_port("hello");
+ `)
+ )[0].getString(0);
+ Assert.equal(value, "hello");
+
+ value = (
+ await db.execute(`
+ SELECT strip_prefix_and_userinfo("hello");
+ `)
+ )[0].getString(0);
+ Assert.equal(value, "hello");
+});
+
+function permute(sets = []) {
+ if (!sets.length) {
+ return [[]];
+ }
+ let firstSet = sets[0];
+ let otherSets = sets.slice(1);
+ let permutedSequences = [];
+ let otherPermutedSequences = permute(otherSets);
+ for (let other of otherPermutedSequences) {
+ for (let value of firstSet) {
+ permutedSequences.push([value].concat(other));
+ }
+ }
+ return permutedSequences;
+}
diff --git a/toolkit/components/places/tests/unit/test_sql_guid_functions.js b/toolkit/components/places/tests/unit/test_sql_guid_functions.js
new file mode 100644
index 0000000000..b4fbdac612
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_sql_guid_functions.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests that the guid function generates a guid of the proper length,
+ * with no invalid characters.
+ */
+
+/**
+ * Checks all our invariants about our guids for a given result.
+ *
+ * @param aGuid
+ * The guid to check.
+ */
+function check_invariants(aGuid) {
+ info("Checking guid '" + aGuid + "'");
+
+ do_check_valid_places_guid(aGuid);
+}
+
+// Test Functions
+
+function test_guid_invariants() {
+ const kExpectedChars = 64;
+ const kAllowedChars =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
+ Assert.equal(kAllowedChars.length, kExpectedChars);
+ const kGuidLength = 12;
+
+ let checkedChars = [];
+ for (let i = 0; i < kGuidLength; i++) {
+ checkedChars[i] = {};
+ for (let j = 0; j < kAllowedChars; j++) {
+ checkedChars[i][kAllowedChars[j]] = false;
+ }
+ }
+
+ // We run this until we've seen every character that we expect to see in every
+ // position.
+ let seenChars = 0;
+ let stmt = DBConn().createStatement("SELECT GENERATE_GUID()");
+ while (seenChars != kExpectedChars * kGuidLength) {
+ Assert.ok(stmt.executeStep());
+ let guid = stmt.getString(0);
+ check_invariants(guid);
+
+ for (let i = 0; i < guid.length; i++) {
+ let character = guid[i];
+ if (!checkedChars[i][character]) {
+ checkedChars[i][character] = true;
+ seenChars++;
+ }
+ }
+ stmt.reset();
+ }
+ stmt.finalize();
+
+ // One last reality check - make sure all of our characters were seen.
+ for (let i = 0; i < kGuidLength; i++) {
+ for (let j = 0; j < kAllowedChars; j++) {
+ Assert.ok(checkedChars[i][kAllowedChars[j]]);
+ }
+ }
+
+ run_next_test();
+}
+
+function test_guid_on_background() {
+ // We should not assert if we execute this asynchronously.
+ let stmt = DBConn().createAsyncStatement("SELECT GENERATE_GUID()");
+ let checked = false;
+ stmt.executeAsync({
+ handleResult(aResult) {
+ try {
+ let row = aResult.getNextRow();
+ check_invariants(row.getResultByIndex(0));
+ Assert.equal(aResult.getNextRow(), null);
+ checked = true;
+ } catch (e) {
+ do_throw(e);
+ }
+ },
+ handleCompletion(aReason) {
+ Assert.equal(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED);
+ Assert.ok(checked);
+ run_next_test();
+ },
+ });
+ stmt.finalize();
+}
+
+// Test Runner
+
+[test_guid_invariants, test_guid_on_background].forEach(fn => add_test(fn));
diff --git a/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js
new file mode 100644
index 0000000000..43f899c237
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js
@@ -0,0 +1,119 @@
+/* -*- 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/. */
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ searches: null,
+
+ minResultsForPopup: 0,
+ timeout: 10,
+ searchParam: "",
+ textValue: "",
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+
+ get searchCount() {
+ return this.searches.length;
+ },
+
+ getSearchAt(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin() {},
+ onSearchComplete() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex(aIndex) {},
+ invalidate() {},
+
+ // nsISupports implementation
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompletePopup"]),
+ },
+
+ // nsISupports implementation
+ QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteInput"]),
+};
+
+async function ensure_tag_results(results, searchTerm) {
+ var controller = Cc["@mozilla.org/autocomplete/controller;1"].getService(
+ Ci.nsIAutoCompleteController
+ );
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["places-tag-autocomplete"]);
+
+ controller.input = input;
+
+ return new Promise(resolve => {
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function input_onSearchBegin() {
+ numSearchesStarted++;
+ Assert.equal(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function input_onSearchComplete() {
+ Assert.equal(numSearchesStarted, 1);
+ if (results.length) {
+ Assert.equal(
+ controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH
+ );
+ } else {
+ Assert.equal(
+ controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH
+ );
+ }
+
+ Assert.equal(controller.matchCount, results.length);
+ for (var i = 0; i < controller.matchCount; i++) {
+ Assert.equal(controller.getValueAt(i), results[i]);
+ }
+
+ resolve();
+ };
+
+ controller.startSearch(searchTerm);
+ });
+}
+
+var uri1 = uri("http://site.tld/1");
+
+var tests = [
+ () => ensure_tag_results(["bar", "Baz", "boo"], "b"),
+ () => ensure_tag_results(["bar", "Baz"], "ba"),
+ () => ensure_tag_results(["bar", "Baz"], "Ba"),
+ () => ensure_tag_results(["bar"], "bar"),
+ () => ensure_tag_results(["Baz"], "Baz"),
+ () => ensure_tag_results([], "barb"),
+ () => ensure_tag_results([], "foo"),
+ () =>
+ ensure_tag_results(["first tag, bar", "first tag, Baz"], "first tag, ba"),
+ () =>
+ ensure_tag_results(
+ ["first tag; bar", "first tag; Baz"],
+ "first tag; ba"
+ ),
+];
+
+/**
+ * Test tag autocomplete
+ */
+add_task(async function test_tag_autocomplete() {
+ PlacesUtils.tagging.tagURI(uri1, ["bar", "Baz", "boo", "*nix"]);
+
+ for (let tagTest of tests) {
+ await tagTest();
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_tagging.js b/toolkit/components/places/tests/unit/test_tagging.js
new file mode 100644
index 0000000000..f38db0d0f6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_tagging.js
@@ -0,0 +1,188 @@
+/* -*- 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/. */
+
+// Notice we use createInstance because later we will have to terminate the
+// service and restart it.
+var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"]
+ .createInstance()
+ .QueryInterface(Ci.nsITaggingService);
+
+function run_test() {
+ var options = PlacesUtils.history.getNewQueryOptions();
+ var query = PlacesUtils.history.getNewQuery();
+
+ query.setParents([PlacesUtils.bookmarks.tagsGuid]);
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var tagRoot = result.root;
+ tagRoot.containerOpen = true;
+
+ Assert.equal(tagRoot.childCount, 0);
+
+ var uri1 = uri("http://foo.tld/");
+ var uri2 = uri("https://bar.tld/");
+
+ // this also tests that the multiple folders are not created for the same tag
+ tagssvc.tagURI(uri1, ["tag 1"]);
+ tagssvc.tagURI(uri2, ["tag 1"]);
+ Assert.equal(tagRoot.childCount, 1);
+
+ var tag1node = tagRoot
+ .getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ var tag1itemId = tag1node.itemId;
+
+ Assert.equal(tag1node.title, "tag 1");
+ tag1node.containerOpen = true;
+ Assert.equal(tag1node.childCount, 2);
+
+ // Tagging the same url twice (or even thrice!) with the same tag should be a
+ // no-op
+ tagssvc.tagURI(uri1, ["tag 1"]);
+ Assert.equal(tag1node.childCount, 2);
+ tagssvc.tagURI(uri1, [tag1itemId]);
+ Assert.equal(tag1node.childCount, 2);
+ Assert.equal(tagRoot.childCount, 1);
+
+ // also tests bug 407575
+ tagssvc.tagURI(uri1, [tag1itemId, "tag 1", "tag 2", "Tag 1", "Tag 2"]);
+ Assert.equal(tagRoot.childCount, 2);
+ Assert.equal(tag1node.childCount, 2);
+
+ // test getTagsForURI
+ var uri1tags = tagssvc.getTagsForURI(uri1);
+ Assert.equal(uri1tags.length, 2);
+ Assert.equal(uri1tags[0], "Tag 1");
+ Assert.equal(uri1tags[1], "Tag 2");
+ var uri2tags = tagssvc.getTagsForURI(uri2);
+ Assert.equal(uri2tags.length, 1);
+ Assert.equal(uri2tags[0], "Tag 1");
+
+ // test untagging
+ tagssvc.untagURI(uri1, ["tag 1"]);
+ Assert.equal(tag1node.childCount, 1);
+
+ let wait = PlacesTestUtils.waitForNotification("bookmark-removed", events =>
+ events.some(event => event.id == tagRoot.itemId)
+ );
+ // removing the last uri from a tag should remove the tag-container
+ tagssvc.untagURI(uri2, ["tag 1"]);
+ wait.then(() => {
+ Assert.equal(tagRoot.childCount, 1);
+ });
+
+ // cleanup
+ tag1node.containerOpen = false;
+
+ // get array of tag folder ids => title
+ // for testing tagging with mixed folder ids and tags
+ var child = tagRoot.getChild(0);
+ var tagId = child.itemId;
+ var tagTitle = child.title;
+
+ // test mixed id/name tagging
+ // as well as non-id numeric tags
+ var uri3 = uri("http://testuri/3");
+ tagssvc.tagURI(uri3, [tagId, "tag 3", "456"]);
+ var tags = tagssvc.getTagsForURI(uri3);
+ Assert.ok(tags.includes(tagTitle));
+ Assert.ok(tags.includes("tag 3"));
+ Assert.ok(tags.includes("456"));
+
+ // test mixed id/name tagging
+ tagssvc.untagURI(uri3, [tagId, "tag 3", "456"]);
+ tags = tagssvc.getTagsForURI(uri3);
+ Assert.equal(tags.length, 0);
+
+ // Terminate tagging service, fire up a new instance and check that existing
+ // tags are there. This will ensure that any internal caching system is
+ // correctly filled at startup and we are not losing previously existing tags.
+ var uri4 = uri("http://testuri/4");
+ tagssvc.tagURI(uri4, [tagId, "tag 3", "456"]);
+ tagssvc = null;
+ tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].getService(
+ Ci.nsITaggingService
+ );
+ var uri4Tags = tagssvc.getTagsForURI(uri4);
+ Assert.equal(uri4Tags.length, 3);
+ Assert.ok(uri4Tags.includes(tagTitle));
+ Assert.ok(uri4Tags.includes("tag 3"));
+ Assert.ok(uri4Tags.includes("456"));
+
+ // Test sparse arrays.
+ let curChildCount = tagRoot.childCount;
+
+ try {
+ tagssvc.tagURI(uri1, [undefined, "tagSparse"]);
+ Assert.equal(tagRoot.childCount, curChildCount + 1);
+ } catch (ex) {
+ do_throw("Passing a sparse array should not throw");
+ }
+ try {
+ wait = PlacesTestUtils.waitForNotification("bookmark-removed", events =>
+ events.some(event => event.id == tagRoot.itemId)
+ );
+ tagssvc.untagURI(uri1, [undefined, "tagSparse"]);
+ wait.then(() => {
+ Assert.equal(tagRoot.childCount, curChildCount);
+ });
+ } catch (ex) {
+ do_throw("Passing a sparse array should not throw");
+ }
+
+ // Test that the API throws for bad arguments.
+ try {
+ tagssvc.tagURI(uri1, ["", "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.untagURI(uri1, ["", "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.tagURI(uri1, [0, "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.tagURI(uri1, [0, "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+
+ // Tag name length should be limited to PlacesUtils.bookmarks.MAX_TAG_LENGTH (bug407821)
+ try {
+ // generate a long tag name. i.e. looooo...oong_tag
+ var n = PlacesUtils.bookmarks.MAX_TAG_LENGTH;
+ var someOos = new Array(n).join("o");
+ var longTagName = "l" + someOos + "ng_tag";
+
+ tagssvc.tagURI(uri1, ["short_tag", longTagName]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ Assert.equal(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+
+ // cleanup
+ tagRoot.containerOpen = false;
+
+ // Tagging service should trim tags (Bug967196)
+ let exampleURI = uri("http://www.example.com/");
+ PlacesUtils.tagging.tagURI(exampleURI, [" test "]);
+
+ let exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI);
+ Assert.equal(exampleTags.length, 1);
+ Assert.equal(exampleTags[0], "test");
+
+ PlacesUtils.tagging.untagURI(exampleURI, ["test"]);
+ exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI);
+ Assert.equal(exampleTags.length, 0);
+}
diff --git a/toolkit/components/places/tests/unit/test_telemetry.js b/toolkit/components/places/tests/unit/test_telemetry.js
new file mode 100644
index 0000000000..b48bb76a7f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -0,0 +1,151 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests common Places telemetry probes by faking the telemetry service.
+
+// Enable the collection (during test) for all products so even products
+// that don't collect the data will be able to run the test without failure.
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+const { PlacesDBUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PlacesDBUtils.sys.mjs"
+);
+
+const histograms = {
+ PLACES_PAGES_COUNT: val => Assert.equal(val, 1),
+ PLACES_BOOKMARKS_COUNT: val => Assert.equal(val, 1),
+ PLACES_TAGS_COUNT: val => Assert.equal(val, 1),
+ PLACES_KEYWORDS_COUNT: val => Assert.equal(val, 1),
+ PLACES_SORTED_BOOKMARKS_PERC: val => Assert.equal(val, 100),
+ PLACES_TAGGED_BOOKMARKS_PERC: val => Assert.equal(val, 100),
+ PLACES_DATABASE_FILESIZE_MB: val => Assert.ok(val > 0),
+ PLACES_DATABASE_FAVICONS_FILESIZE_MB: val => Assert.ok(val > 0),
+ PLACES_EXPIRATION_STEPS_TO_CLEAN2: val => Assert.ok(val > 1),
+ PLACES_IDLE_MAINTENANCE_TIME_MS: val => Assert.ok(val > 0),
+ PLACES_ANNOS_PAGES_COUNT: val => Assert.equal(val, 1),
+ PLACES_MAINTENANCE_DAYSFROMLAST: val => Assert.ok(val >= 0),
+};
+
+const scalars = {
+ pages_need_frecency_recalculation: 1, // 1 bookmark is added causing recalc.
+};
+
+/**
+ * Forces an expiration run.
+ *
+ * @param [optional] aLimit
+ * Limit for the expiration. Pass -1 for unlimited.
+ * Any other non-positive value will just expire orphans.
+ *
+ * @return {Promise}
+ * @resolves When expiration finishes.
+ * @rejects Never.
+ */
+function promiseForceExpirationStep(aLimit) {
+ let promise = promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ let expire = Cc["@mozilla.org/places/expiration;1"].getService(
+ Ci.nsIObserver
+ );
+ expire.observe(null, "places-debug-start-expiration", aLimit);
+ return promise;
+}
+
+/**
+ * Returns a PRTime in the past usable to add expirable visits.
+ *
+ * param [optional] daysAgo
+ * Expiration ignores any visit added in the last 7 days, so by default
+ * this will be set to 7.
+ * @note to be safe against DST issues we go back one day more.
+ */
+function getExpirablePRTime(daysAgo = 7) {
+ let dateObj = new Date();
+ // Normalize to midnight
+ dateObj.setHours(0);
+ dateObj.setMinutes(0);
+ dateObj.setSeconds(0);
+ dateObj.setMilliseconds(0);
+ dateObj = new Date(dateObj.getTime() - (daysAgo + 1) * 86400000);
+ return dateObj.getTime() * 1000;
+}
+
+add_task(async function test_execute() {
+ // Put some trash in the database.
+ let uri = Services.io.newURI("http://moz.org/");
+
+ PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "moz test",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "moz test",
+ url: uri,
+ },
+ ],
+ },
+ ],
+ });
+
+ PlacesUtils.tagging.tagURI(uri, ["tag"]);
+ await PlacesUtils.keywords.insert({ url: uri.spec, keyword: "keyword" });
+
+ // Set a large annotation.
+ let content = "";
+ while (content.length < 1024) {
+ content += "0";
+ }
+
+ await PlacesUtils.history.update({
+ url: uri,
+ annotations: new Map([["test-anno", content]]),
+ });
+
+ await PlacesDBUtils.telemetry();
+
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ // Test expiration probes.
+ let timeInMicroseconds = getExpirablePRTime(8);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ for (let i = 0; i < 3; i++) {
+ await PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://" + i + ".moz.org/"),
+ visitDate: newTimeInMicroseconds(),
+ });
+ }
+ Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
+ await promiseForceExpirationStep(2);
+ await promiseForceExpirationStep(2);
+
+ // Test idle probes.
+ await PlacesDBUtils.maintenanceOnIdle();
+
+ for (let histogramId in histograms) {
+ info("checking histogram " + histogramId);
+ let validate = histograms[histogramId];
+ let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
+ validate(snapshot.sum);
+ Assert.ok(Object.values(snapshot.values).reduce((a, b) => a + b, 0) > 0);
+ }
+ for (let scalarName in scalars) {
+ let scalar = "places." + scalarName;
+ info("checking scalar " + scalar);
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ scalar,
+ scalars[scalarName],
+ "Verify scalar value matches"
+ );
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js
new file mode 100644
index 0000000000..fa03c0e06b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js
@@ -0,0 +1,211 @@
+/* -*- 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/. */
+
+/**
+ * Bug 455315
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=412132
+ *
+ * Ensures that the frecency of a bookmark's URI is what it should be after the
+ * bookmark is deleted.
+ */
+
+add_task(async function removed_bookmark() {
+ info(
+ "After removing bookmark, frecency of bookmark's URI should be " +
+ "zero if URI is unvisited and no longer bookmarked."
+ );
+ const TEST_URI = Services.io.newURI("http://example.com/1");
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("Bookmarked => frecency of URI should be != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.remove(bm);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("Unvisited URI no longer bookmarked => frecency should = 0");
+ Assert.equal(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function removed_but_visited_bookmark() {
+ info(
+ "After removing bookmark, frecency of bookmark's URI should " +
+ "not be zero if URI is visited."
+ );
+ const TEST_URI = Services.io.newURI("http://example.com/1");
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("Bookmarked => frecency of URI should be != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesTestUtils.addVisits(TEST_URI);
+ await PlacesUtils.bookmarks.remove(bm);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("*Visited* URI no longer bookmarked => frecency should != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function remove_bookmark_still_bookmarked() {
+ info(
+ "After removing bookmark, frecency of bookmark's URI should " +
+ "not be zero if URI is still bookmarked."
+ );
+ const TEST_URI = Services.io.newURI("http://example.com/1");
+ let bm1 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 1 title",
+ url: TEST_URI,
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 2 title",
+ url: TEST_URI,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("Bookmarked => frecency of URI should be != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.remove(bm1);
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("URI still bookmarked => frecency should != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function cleared_parent_of_visited_bookmark() {
+ info(
+ "After removing all children from bookmark's parent, frecency " +
+ "of bookmark's URI should not be zero if URI is visited."
+ );
+ const TEST_URI = Services.io.newURI("http://example.com/1");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("Bookmarked => frecency of URI should be != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesTestUtils.addVisits(TEST_URI);
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("*Visited* URI no longer bookmarked => frecency should != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function cleared_parent_of_bookmark_still_bookmarked() {
+ info(
+ "After removing all children from bookmark's parent, frecency " +
+ "of bookmark's URI should not be zero if URI is still " +
+ "bookmarked."
+ );
+ const TEST_URI = Services.io.newURI("http://example.com/1");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "bookmark 1 title",
+ url: TEST_URI,
+ });
+
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bookmark 2 folder",
+ });
+ await PlacesUtils.bookmarks.insert({
+ title: "bookmark 2 title",
+ parentGuid: folder.guid,
+ url: TEST_URI,
+ });
+
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ info("Bookmarked => frecency of URI should be != 0");
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.remove(folder);
+ await PlacesFrecencyRecalculator.recalculateAnyOutdatedFrecencies();
+ // URI still bookmarked => frecency should != 0.
+ Assert.notEqual(
+ await PlacesTestUtils.getDatabaseValue("moz_places", "frecency", {
+ url: TEST_URI,
+ }),
+ 0
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
diff --git a/toolkit/components/places/tests/unit/test_utils_backups_create.js b/toolkit/components/places/tests/unit/test_utils_backups_create.js
new file mode 100644
index 0000000000..a0bc506695
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js
@@ -0,0 +1,152 @@
+/* -*- 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/. */
+
+/**
+ * Check for correct functionality of bookmarks backups
+ */
+
+const NUMBER_OF_BACKUPS = 10;
+
+async function createBackups(nBackups, dateObj, bookmarksBackupDir) {
+ // Generate random dates.
+ let dates = [];
+ while (dates.length < nBackups) {
+ // Use last year to ensure today's backup is the newest.
+ let randomDate = new Date(
+ dateObj.getFullYear() - 1,
+ Math.floor(12 * Math.random()),
+ Math.floor(28 * Math.random())
+ );
+ if (!dates.includes(randomDate.getTime())) {
+ dates.push(randomDate.getTime());
+ }
+ }
+ // Sort dates from oldest to newest.
+ dates.sort();
+
+ // Fake backups are created backwards to ensure we won't consider file
+ // creation time.
+ // Create fake backups for the newest dates.
+ for (let i = dates.length - 1; i >= 0; i--) {
+ let backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i]));
+ let backupFile = bookmarksBackupDir.clone();
+ backupFile.append(backupFilename);
+ backupFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, parseInt("0666", 8));
+ info("Creating fake backup " + backupFile.leafName);
+ if (!backupFile.exists()) {
+ do_throw("Unable to create fake backup " + backupFile.leafName);
+ }
+ }
+
+ return dates;
+}
+
+async function checkBackups(dates, bookmarksBackupDir) {
+ // Check backups. We have 11 dates but we the max number is 10 so the
+ // oldest backup should have been removed.
+ for (let i = 0; i < dates.length; i++) {
+ let backupFilename;
+ let shouldExist;
+ let backupFile;
+ if (i > 0) {
+ let files = bookmarksBackupDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.nextFile;
+ if (PlacesBackups.filenamesRegex.test(entry.leafName)) {
+ backupFilename = entry.leafName;
+ backupFile = entry;
+ break;
+ }
+ }
+ shouldExist = true;
+ } else {
+ backupFilename = PlacesBackups.getFilenameForDate(new Date(dates[i]));
+ backupFile = bookmarksBackupDir.clone();
+ backupFile.append(backupFilename);
+ shouldExist = false;
+ }
+ if (backupFile.exists() != shouldExist) {
+ do_throw(
+ "Backup should " +
+ (shouldExist ? "" : "not") +
+ " exist: " +
+ backupFilename
+ );
+ }
+ }
+}
+
+async function cleanupFiles(bookmarksBackupDir) {
+ // Cleanup backups folder.
+ // XXX: Can't use bookmarksBackupDir.remove(true) because file lock happens
+ // on WIN XP.
+ let files = bookmarksBackupDir.directoryEntries;
+ while (files.hasMoreElements()) {
+ let entry = files.nextFile;
+ entry.remove(false);
+ }
+ // Clear cache to match the manual removing of files
+ delete PlacesBackups._backupFiles;
+ Assert.ok(!bookmarksBackupDir.directoryEntries.hasMoreElements());
+}
+
+add_task(async function test_create_backups() {
+ let backupFolderPath = await PlacesBackups.getBackupFolder();
+ let bookmarksBackupDir = new FileUtils.File(backupFolderPath);
+
+ let dateObj = new Date();
+ let dates = await createBackups(
+ NUMBER_OF_BACKUPS,
+ dateObj,
+ bookmarksBackupDir
+ );
+ // Add today's backup.
+ await PlacesBackups.create(NUMBER_OF_BACKUPS);
+ dates.push(dateObj.getTime());
+ await checkBackups(dates, bookmarksBackupDir);
+ await cleanupFiles(bookmarksBackupDir);
+});
+
+add_task(async function test_saveBookmarks_with_no_backups() {
+ let backupFolderPath = await PlacesBackups.getBackupFolder();
+ let bookmarksBackupDir = new FileUtils.File(backupFolderPath);
+
+ Services.prefs.setIntPref("browser.bookmarks.max_backups", 0);
+
+ let filePath = PathUtils.join(do_get_tempdir().path, "backup.json");
+ await PlacesBackups.saveBookmarksToJSONFile(filePath);
+ let files = bookmarksBackupDir.directoryEntries;
+ Assert.ok(!files.hasMoreElements(), "Should have no backup files.");
+ await IOUtils.remove(filePath);
+ // We don't need to call cleanupFiles as we are not creating any
+ // backups but need to reset the cache.
+ delete PlacesBackups._backupFiles;
+});
+
+add_task(async function test_saveBookmarks_with_backups() {
+ let backupFolderPath = await PlacesBackups.getBackupFolder();
+ let bookmarksBackupDir = new FileUtils.File(backupFolderPath);
+
+ Services.prefs.setIntPref("browser.bookmarks.max_backups", NUMBER_OF_BACKUPS);
+
+ let filePath = PathUtils.join(do_get_tempdir().path, "backup.json");
+ let dateObj = new Date();
+ let dates = await createBackups(
+ NUMBER_OF_BACKUPS,
+ dateObj,
+ bookmarksBackupDir
+ );
+
+ await PlacesBackups.saveBookmarksToJSONFile(filePath);
+
+ let backupPath = await PlacesBackups.getMostRecentBackup();
+ Assert.ok(await IOUtils.read(backupPath, { decompress: true }));
+
+ dates.push(dateObj.getTime());
+ await checkBackups(dates, bookmarksBackupDir);
+ await IOUtils.remove(filePath);
+ await cleanupFiles(bookmarksBackupDir);
+});
diff --git a/toolkit/components/places/tests/unit/test_utils_backups_hasRecent.js b/toolkit/components/places/tests/unit/test_utils_backups_hasRecent.js
new file mode 100644
index 0000000000..77d356b032
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_backups_hasRecent.js
@@ -0,0 +1,43 @@
+/* -*- 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/. */
+
+/**
+ * Check for correct functionality of bookmarks backups
+ */
+
+/**
+ * Creates a fake empty dated backup.
+ * @param {Date} date the Date to use for the backup file name
+ * @param {string} backupsFolderPath the path to the backups folder
+ * @returns path of the created backup file
+ */
+async function createFakeBackup(date, backupsFolderPath) {
+ let backupFilePath = PathUtils.join(
+ backupsFolderPath,
+ PlacesBackups.getFilenameForDate(date)
+ );
+ await IOUtils.write(backupFilePath, new Uint8Array());
+ return backupFilePath;
+}
+
+add_task(async function test_hasRecentBackup() {
+ let backupFolderPath = await PlacesBackups.getBackupFolder();
+ Assert.ok(!(await PlacesBackups.hasRecentBackup()), "Check no recent backup");
+
+ await createFakeBackup(new Date(Date.now() - 4 * 86400), backupFolderPath);
+ Assert.ok(!(await PlacesBackups.hasRecentBackup()), "Check no recent backup");
+ PlacesBackups.invalidateCache();
+ await createFakeBackup(new Date(Date.now() - 2 * 86400), backupFolderPath);
+ Assert.ok(await PlacesBackups.hasRecentBackup(), "Check has recent backup");
+ PlacesBackups.invalidateCache();
+
+ try {
+ await IOUtils.remove(backupFolderPath, { recursive: true });
+ } catch (ex) {
+ // On Windows the files may be locked.
+ info("Unable to cleanup the backups test folder");
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js
new file mode 100644
index 0000000000..2d092ef591
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js
@@ -0,0 +1,266 @@
+/* -*- 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/. */
+
+/**
+ * Check for correct functionality of PlacesUtils.getURLsForContainerNode and
+ * PlacesUtils.hasChildURIs (those helpers share almost all of their code)
+ */
+
+var PU = PlacesUtils;
+var hs = PU.history;
+
+add_task(async function test_getURLsForContainerNode_folder() {
+ info("*** TEST: folder");
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ // This is the folder we will check for children.
+ title: "folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ children: [
+ {
+ title: "inside folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ url: "place:sort=1",
+ title: "inside query",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ var query = hs.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ var options = hs.getNewQueryOptions();
+
+ info("Check folder without uri nodes");
+ check_uri_nodes(query, options, 0);
+
+ info("Check folder with uri nodes");
+ // Add an uri node, this should be considered.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: bookmarks[0].guid,
+ url: "http://www.mozilla.org/",
+ title: "bookmark",
+ });
+ check_uri_nodes(query, options, 1);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_getURLsForContainerNode_folder_excludeItems() {
+ info("*** TEST: folder in an excludeItems root");
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ // This is the folder we will check for children.
+ title: "folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ children: [
+ {
+ title: "inside folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ url: "place:sort=1",
+ title: "inside query",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ var query = hs.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ var options = hs.getNewQueryOptions();
+ options.excludeItems = true;
+
+ info("Check folder without uri nodes");
+ check_uri_nodes(query, options, 0);
+
+ info("Check folder with uri nodes");
+ // Add an uri node, this should be considered.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: bookmarks[0].guid,
+ url: "http://www.mozilla.org/",
+ title: "bookmark",
+ });
+ check_uri_nodes(query, options, 1);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_getURLsForContainerNode_query() {
+ info("*** TEST: query");
+ // This is the query we will check for children.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "inside query",
+ url: `place:parent=${PlacesUtils.bookmarks.menuGuid}&sort=1`,
+ });
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "inside folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ url: "place:sort=1",
+ title: "inside query",
+ },
+ ],
+ },
+ ],
+ });
+
+ var query = hs.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ var options = hs.getNewQueryOptions();
+
+ info("Check query without uri nodes");
+ check_uri_nodes(query, options, 0);
+
+ info("Check query with uri nodes");
+ // Add an uri node, this should be considered.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://www.mozilla.org/",
+ title: "bookmark",
+ });
+ check_uri_nodes(query, options, 1);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_getURLsForContainerNode_query_excludeItems() {
+ info("*** TEST: excludeItems Query");
+ // This is the query we will check for children.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "inside query",
+ url: `place:parent=${PlacesUtils.bookmarks.menuGuid}&sort=1`,
+ });
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "inside folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ url: "place:sort=1",
+ title: "inside query",
+ },
+ ],
+ },
+ ],
+ });
+
+ var query = hs.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ var options = hs.getNewQueryOptions();
+ options.excludeItems = true;
+
+ info("Check folder without uri nodes");
+ check_uri_nodes(query, options, 0);
+
+ info("Check folder with uri nodes");
+ // Add an uri node, this should be considered.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://www.mozilla.org/",
+ title: "bookmark",
+ });
+ check_uri_nodes(query, options, 1);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_getURLsForContainerNode_query_excludeQueries() {
+ info("*** TEST: !expandQueries Query");
+ // This is the query we will check for children.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "inside query",
+ url: `place:parent=${PlacesUtils.bookmarks.menuGuid}&sort=1`,
+ });
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "inside folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ url: "place:sort=1",
+ title: "inside query",
+ },
+ ],
+ },
+ ],
+ });
+
+ var query = hs.getNewQuery();
+ query.setParents([PlacesUtils.bookmarks.toolbarGuid]);
+ var options = hs.getNewQueryOptions();
+ options.expandQueries = false;
+
+ info("Check folder without uri nodes");
+ check_uri_nodes(query, options, 0);
+
+ info("Check folder with uri nodes");
+ // Add an uri node, this should be considered.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://www.mozilla.org/",
+ title: "bookmark",
+ });
+ check_uri_nodes(query, options, 1);
+});
+
+/**
+ * Executes a query and checks number of uri nodes in the first container in
+ * query's results. To correctly test a container ensure that the query will
+ * return only your container in the first level.
+ *
+ * @param aQuery
+ * nsINavHistoryQuery object defining the query
+ * @param aOptions
+ * nsINavHistoryQueryOptions object defining the query's options
+ * @param aExpectedURINodes
+ * number of expected uri nodes
+ */
+function check_uri_nodes(aQuery, aOptions, aExpectedURINodes) {
+ var result = hs.executeQuery(aQuery, aOptions);
+ var root = result.root;
+ root.containerOpen = true;
+ var node = root.getChild(0);
+ Assert.equal(PU.hasChildURIs(node), aExpectedURINodes > 0);
+ Assert.equal(PU.getURLsForContainerNode(node).length, aExpectedURINodes);
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_utils_timeConversion.js b/toolkit/components/places/tests/unit/test_utils_timeConversion.js
new file mode 100644
index 0000000000..c71effe709
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_timeConversion.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Check time conversion utils.
+ */
+
+add_task(async function toDate() {
+ Assert.throws(() => PlacesUtils.toDate(), /Invalid value/, "Should throw");
+ Assert.throws(() => PlacesUtils.toDate(NaN), /Invalid value/, "Should throw");
+ Assert.throws(
+ () => PlacesUtils.toDate(null),
+ /Invalid value/,
+ "Should throw"
+ );
+ Assert.throws(() => PlacesUtils.toDate("1"), /Invalid value/, "Should throw");
+
+ const now = Date.now();
+ const usecs = now * 1000;
+ Assert.deepEqual(PlacesUtils.toDate(usecs), new Date(now));
+});
+
+add_task(async function toPRTime() {
+ Assert.throws(() => PlacesUtils.toPRTime(), /TypeError/, "Should throw");
+ Assert.throws(() => PlacesUtils.toPRTime(null), /TypeError/, "Should throw");
+ Assert.throws(
+ () => PlacesUtils.toPRTime({}),
+ /Invalid value/,
+ "Should throw"
+ );
+ Assert.throws(
+ () => PlacesUtils.toPRTime(NaN),
+ /Invalid value/,
+ "Should throw"
+ );
+ Assert.throws(
+ () => PlacesUtils.toPRTime(new Date(NaN)),
+ /Invalid value/,
+ "Should throw"
+ );
+ Assert.throws(
+ () => PlacesUtils.toPRTime(new URL("https://test.moz")),
+ /Invalid value/,
+ "Should throw"
+ );
+
+ const now = Date.now();
+ const usecs = now * 1000;
+ Assert.strictEqual(PlacesUtils.toPRTime(now), usecs);
+ Assert.strictEqual(PlacesUtils.toPRTime(new Date(now)), usecs);
+});
diff --git a/toolkit/components/places/tests/unit/test_visitsInDB.js b/toolkit/components/places/tests/unit/test_visitsInDB.js
new file mode 100644
index 0000000000..cbd0a04c4b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_visitsInDB.js
@@ -0,0 +1,12 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+add_task(async function test_execute() {
+ const TEST_URI = uri("http://mozilla.com");
+
+ Assert.equal(0, await PlacesTestUtils.visitsInDB(TEST_URI));
+ await PlacesTestUtils.addVisits({ uri: TEST_URI });
+ Assert.equal(1, await PlacesTestUtils.visitsInDB(TEST_URI));
+ await PlacesTestUtils.addVisits({ uri: TEST_URI });
+ Assert.equal(2, await PlacesTestUtils.visitsInDB(TEST_URI));
+});
diff --git a/toolkit/components/places/tests/unit/xpcshell.toml b/toolkit/components/places/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..750e8ad9ea
--- /dev/null
+++ b/toolkit/components/places/tests/unit/xpcshell.toml
@@ -0,0 +1,210 @@
+[DEFAULT]
+head = "head_bookmarks.js"
+firefox-appdir = "browser"
+prefs = ["places.loglevel='All'"]
+support-files = [
+ "bookmarks.corrupt.html",
+ "bookmarks.json",
+ "bookmarks_corrupt.json",
+ "bookmarks.preplaces.html",
+ "bookmarks_html_localized.html",
+ "bookmarks_html_singleframe.html",
+ "bookmarks_iconuri.json",
+ "mobile_bookmarks_folder_import.json",
+ "mobile_bookmarks_folder_merge.json",
+ "mobile_bookmarks_multiple_folders.json",
+ "mobile_bookmarks_root_import.json",
+ "mobile_bookmarks_root_merge.json",
+]
+
+["test_1085291.js"]
+
+["test_1105208.js"]
+
+["test_1105866.js"]
+
+["test_1606731.js"]
+
+["test_331487.js"]
+
+["test_384370.js"]
+
+["test_385397.js"]
+
+["test_399266.js"]
+skip-if = ["os == 'linux'"] # Bug 821781
+
+["test_402799.js"]
+
+["test_412132.js"]
+
+["test_415460.js"]
+
+["test_415757.js"]
+
+["test_419792_node_tags_property.js"]
+
+["test_425563.js"]
+
+["test_429505_remove_shortcuts.js"]
+
+["test_433317_query_title_update.js"]
+
+["test_433525_hasChildren_crash.js"]
+
+["test_454977.js"]
+
+["test_463863.js"]
+
+["test_485442_crash_bug_nsNavHistoryQuery_GetUri.js"]
+
+["test_486978_sort_by_date_queries.js"]
+
+["test_536081.js"]
+
+["test_PlacesDBUtils_removeOldCorruptDBs.js"]
+
+["test_PlacesQuery_history.js"]
+
+["test_PlacesUtils_isRootItem.js"]
+
+["test_PlacesUtils_unwrapNodes_place.js"]
+
+["test_asyncExecuteLegacyQueries.js"]
+
+["test_async_transactions.js"]
+
+["test_autocomplete_match_fallbackTitle.js"]
+
+["test_bookmark-tags-changed_frequency.js"]
+
+["test_bookmarks_html.js"]
+
+["test_bookmarks_html_corrupt.js"]
+
+["test_bookmarks_html_escape_entities.js"]
+
+["test_bookmarks_html_import_tags.js"]
+
+["test_bookmarks_html_localized.js"]
+
+["test_bookmarks_html_singleframe.js"]
+
+["test_bookmarks_json.js"]
+
+["test_bookmarks_json_corrupt.js"]
+
+["test_bookmarks_restore_notification.js"]
+
+["test_broken_folderShortcut_result.js"]
+
+["test_browserhistory.js"]
+
+["test_childlessTags.js"]
+
+["test_frecency_decay.js"]
+
+["test_frecency_observers.js"]
+
+["test_frecency_origins_alternative.js"]
+
+["test_frecency_origins_recalc.js"]
+prefs = ["places.frecency.origins.alternative.featureGate=true"]
+
+["test_frecency_pages_alternative.js"]
+prefs = ["places.frecency.pages.alternative.featureGate=true"]
+
+["test_frecency_pages_recalc_alt.js"]
+prefs = ["places.frecency.pages.alternative.featureGate=true"]
+
+["test_frecency_recalc_triggers.js"]
+
+["test_frecency_recalculator.js"]
+
+["test_frecency_unvisited_bookmark.js"]
+
+["test_frecency_zero_updated.js"]
+
+["test_getChildIndex.js"]
+
+["test_get_query_param_sql_function.js"]
+
+["test_hash.js"]
+
+["test_history.js"]
+
+["test_history_clear.js"]
+
+["test_history_notifications.js"]
+
+["test_history_observer.js"]
+
+["test_history_sidebar.js"]
+
+["test_import_mobile_bookmarks.js"]
+
+["test_isPageInDB.js"]
+
+["test_isURIVisited.js"]
+
+["test_isvisited.js"]
+
+["test_keywords.js"]
+
+["test_lastModified.js"]
+
+["test_markpageas.js"]
+
+["test_metadata.js"]
+
+["test_missing_builtin_folders.js"]
+
+["test_missing_root_folder.js"]
+
+["test_multi_observation.js"]
+
+["test_multi_word_tags.js"]
+
+["test_nested_notifications.js"]
+
+["test_nsINavHistoryViewer.js"]
+
+["test_null_interfaces.js"]
+
+["test_origins.js"]
+
+["test_origins_parsing.js"]
+
+["test_pageGuid_bookmarkGuid.js"]
+
+["test_placeURIs.js"]
+
+["test_promiseBookmarksTree.js"]
+
+["test_resolveNullBookmarkTitles.js"]
+
+["test_result_sort.js"]
+
+["test_resultsAsVisit_details.js"]
+
+["test_sql_function_origin.js"]
+
+["test_sql_guid_functions.js"]
+
+["test_tag_autocomplete_search.js"]
+
+["test_tagging.js"]
+
+["test_telemetry.js"]
+
+["test_update_frecency_after_delete.js"]
+
+["test_utils_backups_create.js"]
+
+["test_utils_backups_hasRecent.js"]
+
+["test_utils_getURLsForContainerNode.js"]
+
+["test_utils_timeConversion.js"]
+
+["test_visitsInDB.js"]