summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/unit
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/unit')
-rw-r--r--toolkit/components/places/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.corrupt.html36
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.json1
-rw-r--r--toolkit/components/places/tests/unit/bookmarks.preplaces.html35
-rw-r--r--toolkit/components/places/tests/unit/bookmarks_html_singleframe.html10
-rw-r--r--toolkit/components/places/tests/unit/bug476292.sqlitebin0 -> 139264 bytes
-rw-r--r--toolkit/components/places/tests/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--toolkit/components/places/tests/unit/default.sqlitebin0 -> 1081344 bytes
-rw-r--r--toolkit/components/places/tests/unit/head_bookmarks.js20
-rw-r--r--toolkit/components/places/tests/unit/livemark.xml17
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json1
-rw-r--r--toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json1
-rw-r--r--toolkit/components/places/tests/unit/nsDummyObserver.js48
-rw-r--r--toolkit/components/places/tests/unit/nsDummyObserver.manifest4
-rw-r--r--toolkit/components/places/tests/unit/places.sparse.sqlitebin0 -> 221184 bytes
-rw-r--r--toolkit/components/places/tests/unit/test_000_frecency.js273
-rw-r--r--toolkit/components/places/tests/unit/test_1085291.js42
-rw-r--r--toolkit/components/places/tests/unit/test_1105208.js24
-rw-r--r--toolkit/components/places/tests/unit/test_1105866.js63
-rw-r--r--toolkit/components/places/tests/unit/test_317472.js65
-rw-r--r--toolkit/components/places/tests/unit/test_331487.js95
-rw-r--r--toolkit/components/places/tests/unit/test_384370.js173
-rw-r--r--toolkit/components/places/tests/unit/test_385397.js142
-rw-r--r--toolkit/components/places/tests/unit/test_399264_query_to_string.js51
-rw-r--r--toolkit/components/places/tests/unit/test_399264_string_to_query.js75
-rw-r--r--toolkit/components/places/tests/unit/test_399266.js78
-rw-r--r--toolkit/components/places/tests/unit/test_402799.js62
-rw-r--r--toolkit/components/places/tests/unit/test_405497.js57
-rw-r--r--toolkit/components/places/tests/unit/test_408221.js165
-rw-r--r--toolkit/components/places/tests/unit/test_412132.js136
-rw-r--r--toolkit/components/places/tests/unit/test_413784.js118
-rw-r--r--toolkit/components/places/tests/unit/test_415460.js43
-rw-r--r--toolkit/components/places/tests/unit/test_415757.js102
-rw-r--r--toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js143
-rw-r--r--toolkit/components/places/tests/unit/test_419731.js96
-rw-r--r--toolkit/components/places/tests/unit/test_419792_node_tags_property.js49
-rw-r--r--toolkit/components/places/tests/unit/test_425563.js74
-rw-r--r--toolkit/components/places/tests/unit/test_429505_remove_shortcuts.js35
-rw-r--r--toolkit/components/places/tests/unit/test_433317_query_title_update.js38
-rw-r--r--toolkit/components/places/tests/unit/test_433525_hasChildren_crash.js56
-rw-r--r--toolkit/components/places/tests/unit/test_452777.js36
-rw-r--r--toolkit/components/places/tests/unit/test_454977.js124
-rw-r--r--toolkit/components/places/tests/unit/test_463863.js60
-rw-r--r--toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.js21
-rw-r--r--toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js129
-rw-r--r--toolkit/components/places/tests/unit/test_536081.js56
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js133
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js77
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js25
-rw-r--r--toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js47
-rw-r--r--toolkit/components/places/tests/unit/test_adaptive.js406
-rw-r--r--toolkit/components/places/tests/unit/test_adaptive_bug527311.js141
-rw-r--r--toolkit/components/places/tests/unit/test_analyze.js28
-rw-r--r--toolkit/components/places/tests/unit/test_annotations.js363
-rw-r--r--toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js95
-rw-r--r--toolkit/components/places/tests/unit/test_async_history_api.js1118
-rw-r--r--toolkit/components/places/tests/unit/test_async_in_batchmode.js55
-rw-r--r--toolkit/components/places/tests/unit/test_async_transactions.js1739
-rw-r--r--toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js39
-rw-r--r--toolkit/components/places/tests/unit/test_bookmark_catobs.js57
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html.js385
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js143
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js57
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js32
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_json.js241
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js325
-rw-r--r--toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js44
-rw-r--r--toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js79
-rw-r--r--toolkit/components/places/tests/unit/test_browserhistory.js129
-rw-r--r--toolkit/components/places/tests/unit/test_bug636917_isLivemark.js35
-rw-r--r--toolkit/components/places/tests/unit/test_childlessTags.js117
-rw-r--r--toolkit/components/places/tests/unit/test_corrupt_telemetry.js31
-rw-r--r--toolkit/components/places/tests/unit/test_crash_476292.js28
-rw-r--r--toolkit/components/places/tests/unit/test_database_replaceOnStartup.js46
-rw-r--r--toolkit/components/places/tests/unit/test_download_history.js283
-rw-r--r--toolkit/components/places/tests/unit/test_frecency.js294
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_observers.js84
-rw-r--r--toolkit/components/places/tests/unit/test_frecency_zero_updated.js30
-rw-r--r--toolkit/components/places/tests/unit/test_getChildIndex.js69
-rw-r--r--toolkit/components/places/tests/unit/test_getPlacesInfo.js112
-rw-r--r--toolkit/components/places/tests/unit/test_history.js184
-rw-r--r--toolkit/components/places/tests/unit/test_history_autocomplete_tags.js185
-rw-r--r--toolkit/components/places/tests/unit/test_history_catobs.js55
-rw-r--r--toolkit/components/places/tests/unit/test_history_clear.js169
-rw-r--r--toolkit/components/places/tests/unit/test_history_notifications.js38
-rw-r--r--toolkit/components/places/tests/unit/test_history_observer.js215
-rw-r--r--toolkit/components/places/tests/unit/test_history_sidebar.js447
-rw-r--r--toolkit/components/places/tests/unit/test_hosts_triggers.js226
-rw-r--r--toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js292
-rw-r--r--toolkit/components/places/tests/unit/test_isPageInDB.js10
-rw-r--r--toolkit/components/places/tests/unit/test_isURIVisited.js84
-rw-r--r--toolkit/components/places/tests/unit/test_isvisited.js75
-rw-r--r--toolkit/components/places/tests/unit/test_keywords.js548
-rw-r--r--toolkit/components/places/tests/unit/test_lastModified.js34
-rw-r--r--toolkit/components/places/tests/unit/test_markpageas.js61
-rw-r--r--toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js514
-rw-r--r--toolkit/components/places/tests/unit/test_multi_queries.js53
-rw-r--r--toolkit/components/places/tests/unit/test_multi_word_tags.js150
-rw-r--r--toolkit/components/places/tests/unit/test_nsINavHistoryViewer.js256
-rw-r--r--toolkit/components/places/tests/unit/test_null_interfaces.js98
-rw-r--r--toolkit/components/places/tests/unit/test_onItemChanged_tags.js52
-rw-r--r--toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js179
-rw-r--r--toolkit/components/places/tests/unit/test_placeURIs.js42
-rw-r--r--toolkit/components/places/tests/unit/test_placesTxn.js937
-rw-r--r--toolkit/components/places/tests/unit/test_preventive_maintenance.js1356
-rw-r--r--toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js50
-rw-r--r--toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js46
-rw-r--r--toolkit/components/places/tests/unit/test_promiseBookmarksTree.js256
-rw-r--r--toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js49
-rw-r--r--toolkit/components/places/tests/unit/test_result_sort.js139
-rw-r--r--toolkit/components/places/tests/unit/test_resultsAsVisit_details.js85
-rw-r--r--toolkit/components/places/tests/unit/test_sql_guid_functions.js106
-rw-r--r--toolkit/components/places/tests/unit/test_svg_favicon.js31
-rw-r--r--toolkit/components/places/tests/unit/test_sync_utils.js1150
-rw-r--r--toolkit/components/places/tests/unit/test_tag_autocomplete_search.js137
-rw-r--r--toolkit/components/places/tests/unit/test_tagging.js189
-rw-r--r--toolkit/components/places/tests/unit/test_telemetry.js166
-rw-r--r--toolkit/components/places/tests/unit/test_update_frecency_after_delete.js151
-rw-r--r--toolkit/components/places/tests/unit/test_utils_backups_create.js90
-rw-r--r--toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js180
-rw-r--r--toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js79
-rw-r--r--toolkit/components/places/tests/unit/test_visitsInDB.js12
-rw-r--r--toolkit/components/places/tests/unit/xpcshell.ini163
126 files changed, 19090 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/unit/.eslintrc.js b/toolkit/components/places/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/places/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
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 000000000..3cf43367f
--- /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 000000000..afe62abae
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.json
@@ -0,0 +1 @@
+{"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":"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":"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":"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/POSTData","flags":0,"expires":4,"mimeType":null,"type":3,"value":"hidden1%3Dbar&text1%3D%25s"},{"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"}]}]},{"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":"placesInternal/READ_ONLY","flags":0,"expires":4,"mimeType":null,"type":1,"value":1},{"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/"}]}]}
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 000000000..2e5a1baf0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bookmarks.preplaces.html
@@ -0,0 +1,35 @@
+<!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>
+<DD>Livemark test comment
+ </DL><p>
+</DL><p>
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 000000000..9fe662f32
--- /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/bug476292.sqlite b/toolkit/components/places/tests/unit/bug476292.sqlite
new file mode 100644
index 000000000..43130cb51
--- /dev/null
+++ b/toolkit/components/places/tests/unit/bug476292.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/unit/corruptDB.sqlite b/toolkit/components/places/tests/unit/corruptDB.sqlite
new file mode 100644
index 000000000..b234246ca
--- /dev/null
+++ b/toolkit/components/places/tests/unit/corruptDB.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/unit/default.sqlite b/toolkit/components/places/tests/unit/default.sqlite
new file mode 100644
index 000000000..8fbd3bc9a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/default.sqlite
Binary files differ
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 000000000..842a66b31
--- /dev/null
+++ b/toolkit/components/places/tests/unit/head_bookmarks.js
@@ -0,0 +1,20 @@
+/* -*- 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/. */
+
+var Ci = Components.interfaces;
+var Cc = Components.classes;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Import common head.
+{
+ 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.
diff --git a/toolkit/components/places/tests/unit/livemark.xml b/toolkit/components/places/tests/unit/livemark.xml
new file mode 100644
index 000000000..db2ea9023
--- /dev/null
+++ b/toolkit/components/places/tests/unit/livemark.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+ <title>Livemark Feed</title>
+ <link href="https://example.com/"/>
+ <updated>2016-08-09T19:51:45.147Z</updated>
+ <author>
+ <name>John Doe</name>
+ </author>
+ <id>urn:uuid:e7947414-6ee0-4009-ae75-8b0ad3c6894b</id>
+ <entry>
+ <title>Some awesome article</title>
+ <link href="https://example.com/some-article"/>
+ <id>urn:uuid:d72ce019-0a56-4a0b-ac03-f66117d78141</id>
+ <updated>2016-08-09T19:57:22.178Z</updated>
+ <summary>My great article summary.</summary>
+ </entry>
+</feed>
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 000000000..38762b3f1
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_import.json
@@ -0,0 +1 @@
+{"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"}]}]} \ No newline at end of file
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 000000000..7319a3a52
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_folder_merge.json
@@ -0,0 +1 @@
+{"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"}]} \ No newline at end of file
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 000000000..afe13c975
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_multiple_folders.json
@@ -0,0 +1 @@
+{"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/"}]}]} \ No newline at end of file
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 000000000..27f5825ec
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_import.json
@@ -0,0 +1 @@
+{"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/"}]}]} \ No newline at end of file
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 000000000..85721f2fa
--- /dev/null
+++ b/toolkit/components/places/tests/unit/mobile_bookmarks_root_merge.json
@@ -0,0 +1 @@
+{"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/"}]}]} \ No newline at end of file
diff --git a/toolkit/components/places/tests/unit/nsDummyObserver.js b/toolkit/components/places/tests/unit/nsDummyObserver.js
new file mode 100644
index 000000000..9049d04b3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/nsDummyObserver.js
@@ -0,0 +1,48 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+// Dummy boomark/history observer
+function DummyObserver() {
+ Services.obs.notifyObservers(null, "dummy-observer-created", null);
+}
+
+DummyObserver.prototype = {
+ // history observer
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID, aTransitionType) {
+ Services.obs.notifyObservers(null, "dummy-observer-visited", null);
+ },
+ onTitleChanged: function () {},
+ onDeleteURI: function () {},
+ onClearHistory: function () {},
+ onPageChanged: function () {},
+ onDeleteVisits: function () {},
+
+ // bookmark observer
+ // onBeginUpdateBatch: function() {},
+ // onEndUpdateBatch: function() {},
+ onItemAdded: function(aItemId, aParentId, aIndex, aItemType, aURI) {
+ Services.obs.notifyObservers(null, "dummy-observer-item-added", null);
+ },
+ onItemChanged: function () {},
+ onItemRemoved: function() {},
+ onItemVisited: function() {},
+ onItemMoved: function() {},
+
+ classID: Components.ID("62e221d3-68c3-4e1a-8943-a27beb5005fe"),
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ Ci.nsINavHistoryObserver,
+ ])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DummyObserver]);
diff --git a/toolkit/components/places/tests/unit/nsDummyObserver.manifest b/toolkit/components/places/tests/unit/nsDummyObserver.manifest
new file mode 100644
index 000000000..ed4d87fff
--- /dev/null
+++ b/toolkit/components/places/tests/unit/nsDummyObserver.manifest
@@ -0,0 +1,4 @@
+component 62e221d3-68c3-4e1a-8943-a27beb5005fe nsDummyObserver.js
+contract @mozilla.org/places/test/dummy-observer;1 62e221d3-68c3-4e1a-8943-a27beb5005fe
+category bookmark-observers nsDummyObserver @mozilla.org/places/test/dummy-observer;1
+category history-observers nsDummyObserver @mozilla.org/places/test/dummy-observer;1
diff --git a/toolkit/components/places/tests/unit/places.sparse.sqlite b/toolkit/components/places/tests/unit/places.sparse.sqlite
new file mode 100644
index 000000000..915089021
--- /dev/null
+++ b/toolkit/components/places/tests/unit/places.sparse.sqlite
Binary files differ
diff --git a/toolkit/components/places/tests/unit/test_000_frecency.js b/toolkit/components/places/tests/unit/test_000_frecency.js
new file mode 100644
index 000000000..0a7347a02
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_000_frecency.js
@@ -0,0 +1,273 @@
+/* -*- 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/. */
+
+/*
+
+Autocomplete Frecency Tests
+
+- add a visit for each score permutation
+- search
+- test number of matches
+- test each item's location in results
+
+*/
+
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+} catch (ex) {
+ do_throw("Could not get services\n");
+}
+
+var bucketPrefs = [
+ [ "firstBucketCutoff", "firstBucketWeight"],
+ [ "secondBucketCutoff", "secondBucketWeight"],
+ [ "thirdBucketCutoff", "thirdBucketWeight"],
+ [ "fourthBucketCutoff", "fourthBucketWeight"],
+ [ null, "defaultBucketWeight"]
+];
+
+var bonusPrefs = {
+ embedVisitBonus: Ci.nsINavHistoryService.TRANSITION_EMBED,
+ framedLinkVisitBonus: Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK,
+ linkVisitBonus: Ci.nsINavHistoryService.TRANSITION_LINK,
+ typedVisitBonus: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ bookmarkVisitBonus: Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
+ downloadVisitBonus: Ci.nsINavHistoryService.TRANSITION_DOWNLOAD,
+ permRedirectVisitBonus: Ci.nsINavHistoryService.TRANSITION_REDIRECT_PERMANENT,
+ tempRedirectVisitBonus: Ci.nsINavHistoryService.TRANSITION_REDIRECT_TEMPORARY,
+ reloadVisitBonus: Ci.nsINavHistoryService.TRANSITION_RELOAD,
+};
+
+// create test data
+var searchTerm = "frecency";
+var results = [];
+var matchCount = 0;
+var now = Date.now();
+var prefPrefix = "places.frecency.";
+
+function* task_initializeBucket(bucket) {
+ let [cutoffName, weightName] = bucket;
+ // get pref values
+ var weight = 0, cutoff = 0;
+ try {
+ weight = prefs.getIntPref(prefPrefix + weightName);
+ } catch (ex) {}
+ try {
+ cutoff = prefs.getIntPref(prefPrefix + cutoffName);
+ } catch (ex) {}
+
+ if (cutoff < 1)
+ return;
+
+ // generate a date within the cutoff period
+ var dateInPeriod = (now - ((cutoff - 1) * 86400 * 1000)) * 1000;
+
+ for (let [bonusName, visitType] of Object.entries(bonusPrefs)) {
+ var frecency = -1;
+ var calculatedURI = null;
+ var matchTitle = "";
+ var bonusValue = prefs.getIntPref(prefPrefix + bonusName);
+ // unvisited (only for first cutoff date bucket)
+ if (bonusName == "unvisitedBookmarkBonus" || bonusName == "unvisitedTypedBonus") {
+ if (cutoffName == "firstBucketCutoff") {
+ let points = Math.ceil(bonusValue / parseFloat(100.0) * weight);
+ var visitCount = 1; // bonusName == "unvisitedBookmarkBonus" ? 1 : 0;
+ frecency = Math.ceil(visitCount * points);
+ calculatedURI = uri("http://" + searchTerm + ".com/" +
+ bonusName + ":" + bonusValue + "/cutoff:" + cutoff +
+ "/weight:" + weight + "/frecency:" + frecency);
+ if (bonusName == "unvisitedBookmarkBonus") {
+ matchTitle = searchTerm + "UnvisitedBookmark";
+ bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, calculatedURI, bmsvc.DEFAULT_INDEX, matchTitle);
+ }
+ else {
+ matchTitle = searchTerm + "UnvisitedTyped";
+ yield PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: now
+ });
+ histsvc.markPageAsTyped(calculatedURI);
+ }
+ }
+ }
+ else {
+ // visited
+ // visited bookmarks get the visited bookmark bonus twice
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK)
+ bonusValue = bonusValue * 2;
+
+ let points = Math.ceil(1 * ((bonusValue / parseFloat(100.000000)).toFixed(6) * weight) / 1);
+ if (!points) {
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_EMBED ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_DOWNLOAD ||
+ visitType == Ci.nsINavHistoryService.TRANSITION_RELOAD ||
+ bonusName == "defaultVisitBonus")
+ frecency = 0;
+ else
+ frecency = -1;
+ }
+ else
+ frecency = points;
+ calculatedURI = uri("http://" + searchTerm + ".com/" +
+ bonusName + ":" + bonusValue + "/cutoff:" + cutoff +
+ "/weight:" + weight + "/frecency:" + frecency);
+ if (visitType == Ci.nsINavHistoryService.TRANSITION_BOOKMARK) {
+ matchTitle = searchTerm + "Bookmarked";
+ bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder, calculatedURI, bmsvc.DEFAULT_INDEX, matchTitle);
+ }
+ else
+ matchTitle = calculatedURI.spec.substr(calculatedURI.spec.lastIndexOf("/")+1);
+ yield PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ transition: visitType,
+ visitDate: dateInPeriod
+ });
+ }
+
+ if (calculatedURI && frecency) {
+ results.push([calculatedURI, frecency, matchTitle]);
+ yield PlacesTestUtils.addVisits({
+ uri: calculatedURI,
+ title: matchTitle,
+ transition: visitType,
+ visitDate: dateInPeriod
+ });
+ }
+ }
+}
+
+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: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+add_task(function* test_frecency()
+{
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+ for (let bucket of bucketPrefs) {
+ yield task_initializeBucket(bucket);
+ }
+
+ // sort results by frecency
+ results.sort((a, b) => b[1] - a[1]);
+ // Make sure there's enough results returned
+ prefs.setIntPref("browser.urlbar.maxRichResults", results.length);
+
+ // DEBUG
+ // results.every(function(el) { dump("result: " + el[1] + ": " + el[0].spec + " (" + el[2] + ")\n"); return true; })
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ // always search in history + bookmarks, no matter what the default is
+ prefs.setIntPref("browser.urlbar.search.sources", 3);
+ prefs.setIntPref("browser.urlbar.default.behavior", 0);
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ let deferred = Promise.defer();
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+
+ // test that all records with non-zero frecency were matched
+ do_check_eq(controller.matchCount, results.length);
+
+ // test that matches are sorted by frecency
+ for (var i = 0; i < controller.matchCount; i++) {
+ let searchURL = controller.getValueAt(i);
+ let expectURL = results[i][0].spec;
+ if (searchURL == expectURL) {
+ do_check_eq(controller.getValueAt(i), results[i][0].spec);
+ do_check_eq(controller.getCommentAt(i), results[i][2]);
+ } else {
+ // If the results didn't match exactly, perhaps it's still the right
+ // frecency just in the wrong "order" (order of same frecency is
+ // undefined), so check if frecency matches. This is okay because we
+ // can still ensure the correct number of expected frecencies.
+ let getFrecency = aURL => aURL.match(/frecency:(-?\d+)$/)[1];
+ print("### checking for same frecency between '" + searchURL +
+ "' and '" + expectURL + "'");
+ do_check_eq(getFrecency(searchURL), getFrecency(expectURL));
+ }
+ }
+ deferred.resolve();
+ };
+
+ controller.startSearch(searchTerm);
+
+ yield deferred.promise;
+});
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 000000000..3159ff8bc
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1085291.js
@@ -0,0 +1,42 @@
+add_task(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.toolbarFolderId).root;
+
+ function* insertAndTest(bmInfo) {
+ bmInfo = yield 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.
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_BOOKMARK
+ , title: "Test Bookmark"
+ , url: "http://test.url.tld" });
+
+ // place: query
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_BOOKMARK
+ , title: "Test Query"
+ , url: "place:folder=BOOKMARKS_MENU" });
+
+ // folder
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_FOLDER
+ , title: "Test Folder" });
+
+ // separator
+ yield insertAndTest({ parentGuid: root.bookmarkGuid
+ , type: PlacesUtils.bookmarks.TYPE_SEPARATOR });
+
+ root.containerOpen = false;
+});
+
+function run_test() {
+ run_next_test();
+}
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 000000000..39a27c95f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1105208.js
@@ -0,0 +1,24 @@
+// Test that result node for folder shortcuts get the target folder title if
+// the shortcut itself has no title set.
+add_task(function* () {
+ let shortcutInfo = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "place:folder=TOOLBAR"
+ });
+
+ let unfiledRoot =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.equal(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+
+ let toolbarInfo =
+ yield PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid);
+ Assert.equal(shortcutNode.title, toolbarInfo.title);
+
+ unfiledRoot.containerOpen = false;
+});
+
+function run_test() {
+ run_next_test();
+}
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 000000000..eb376bbe2
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_1105866.js
@@ -0,0 +1,63 @@
+add_task(function* test_folder_shortcuts() {
+ let shortcutInfo = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "place:folder=TOOLBAR"
+ });
+
+ let unfiledRoot =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(shortcutNode.itemId,
+ yield PlacesUtils.promiseItemId(shortcutInfo.guid));
+ Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).folderItemId,
+ PlacesUtils.toolbarFolderId);
+ 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 = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "place:folder=BOOKMARKS_MENU"
+ });
+ shortcutNode = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(shortcutNode.itemId,
+ yield PlacesUtils.promiseItemId(shortcutInfo.guid));
+ Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).folderItemId,
+ PlacesUtils.bookmarksMenuFolderId);
+ Assert.strictEqual(shortcutNode.bookmarkGuid, shortcutInfo.guid);
+ Assert.strictEqual(PlacesUtils.asQuery(shortcutNode).targetFolderGuid,
+ PlacesUtils.bookmarks.menuGuid);
+
+ unfiledRoot.containerOpen = false;
+});
+
+add_task(function* test_plain_folder() {
+ let folderInfo = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+
+ let unfiledRoot =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ let lastChild = unfiledRoot.getChild(unfiledRoot.childCount - 1);
+ Assert.strictEqual(lastChild.bookmarkGuid, folderInfo.guid);
+ Assert.strictEqual(PlacesUtils.asQuery(lastChild).targetFolderGuid,
+ folderInfo.guid);
+});
+
+add_task(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, "");
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_317472.js b/toolkit/components/places/tests/unit/test_317472.js
new file mode 100644
index 000000000..a08651916
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_317472.js
@@ -0,0 +1,65 @@
+/* -*- 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 charset = "UTF-8";
+const CHARSET_ANNO = "URIProperties/characterSet";
+
+const TEST_URI = uri("http://foo.com");
+const TEST_BOOKMARKED_URI = uri("http://bar.com");
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // add pages to history
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ yield PlacesTestUtils.addVisits(TEST_BOOKMARKED_URI);
+
+ // create bookmarks on TEST_BOOKMARKED_URI
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId,
+ TEST_BOOKMARKED_URI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_BOOKMARKED_URI.spec);
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.toolbarFolderId,
+ TEST_BOOKMARKED_URI, PlacesUtils.bookmarks.DEFAULT_INDEX,
+ TEST_BOOKMARKED_URI.spec);
+
+ // set charset on not-bookmarked page
+ yield PlacesUtils.setCharsetForURI(TEST_URI, charset);
+ // set charset on bookmarked page
+ yield PlacesUtils.setCharsetForURI(TEST_BOOKMARKED_URI, charset);
+
+ // check that we have created a page annotation
+ do_check_eq(PlacesUtils.annotations.getPageAnnotation(TEST_URI, CHARSET_ANNO), charset);
+
+ // get charset from not-bookmarked page
+ do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_URI)), charset);
+
+ // get charset from bookmarked page
+ do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset);
+
+ yield PlacesTestUtils.clearHistory();
+
+ // ensure that charset has gone for not-bookmarked page
+ do_check_neq((yield PlacesUtils.getCharsetForURI(TEST_URI)), charset);
+
+ // check that page annotation has been removed
+ try {
+ PlacesUtils.annotations.getPageAnnotation(TEST_URI, CHARSET_ANNO);
+ do_throw("Charset page annotation has not been removed correctly");
+ } catch (e) {}
+
+ // ensure that charset still exists for bookmarked page
+ do_check_eq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset);
+
+ // remove charset from bookmark and check that has gone
+ yield PlacesUtils.setCharsetForURI(TEST_BOOKMARKED_URI, "");
+ do_check_neq((yield PlacesUtils.getCharsetForURI(TEST_BOOKMARKED_URI)), charset);
+});
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 000000000..55d41aebf
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_331487.js
@@ -0,0 +1,95 @@
+/* -*- 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");
+}
+
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+// main
+function run_test() {
+ // add a folder
+ var folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX);
+
+ // add a bookmark to the folder
+ var b1 = bmsvc.insertBookmark(folder, uri("http://a1.com/"),
+ bmsvc.DEFAULT_INDEX, "1 title");
+ // add a subfolder
+ var sf1 = bmsvc.createFolder(folder, "subfolder 1", bmsvc.DEFAULT_INDEX);
+
+ // add a bookmark to the subfolder
+ var b2 = bmsvc.insertBookmark(sf1, uri("http://a2.com/"),
+ bmsvc.DEFAULT_INDEX, "2 title");
+
+ // add a subfolder to the subfolder
+ var sf2 = bmsvc.createFolder(sf1, "subfolder 2", bmsvc.DEFAULT_INDEX);
+
+ // add a bookmark to the subfolder of the subfolder
+ var b3 = bmsvc.insertBookmark(sf2, uri("http://a3.com/"),
+ bmsvc.DEFAULT_INDEX, "3 title");
+
+ // 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.setFolders([folder], 1);
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).itemId, b1);
+ do_check_eq(root.getChild(1).itemId, sf1);
+
+ // check the contents of the subfolder
+ var sf1Node = root.getChild(1);
+ sf1Node = sf1Node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ sf1Node.containerOpen = true;
+ do_check_eq(sf1Node.childCount, 2);
+ do_check_eq(sf1Node.getChild(0).itemId, b2);
+ do_check_eq(sf1Node.getChild(1).itemId, sf2);
+
+ // check the contents of the subfolder's subfolder
+ var sf2Node = sf1Node.getChild(1);
+ sf2Node = sf2Node.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ sf2Node.containerOpen = true;
+ do_check_eq(sf2Node.childCount, 1);
+ do_check_eq(sf2Node.getChild(0).itemId, 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.setFolders([folder], 1);
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 3);
+ do_check_eq(root.getChild(0).itemId, b1);
+ do_check_eq(root.getChild(1).itemId, b2);
+ do_check_eq(root.getChild(2).itemId, 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 000000000..ec6f43683
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_384370.js
@@ -0,0 +1,173 @@
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+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" }
+];
+
+function run_test() {
+ run_next_test();
+}
+
+/*
+ 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(function* () {
+ // Remove eventual bookmarks.exported.json.
+ let jsonFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.json");
+ if ((yield OS.File.exists(jsonFile)))
+ yield OS.File.remove(jsonFile);
+
+ // 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 = OS.Path.join(do_get_cwd().path, "bookmarks.preplaces.html");
+ yield BookmarkHTMLUtils.importFromFile(htmlFile, true);
+
+ // Populate the database.
+ for (let { uri, tags } of tagData) {
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
+ for (let { uri, title } of bookmarkData) {
+ yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title });
+ }
+ for (let { uri, title } of bookmarkData) {
+ yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: uri,
+ title });
+ }
+
+ yield validate();
+
+ // Test exporting a Places canonical json file.
+ // 1. export to bookmarks.exported.json
+ yield BookmarkJSONUtils.exportToFile(jsonFile);
+ do_print("exported json");
+
+ // 2. empty bookmarks db
+ // 3. import bookmarks.exported.json
+ yield BookmarkJSONUtils.importFromFile(jsonFile, true);
+ do_print("imported json");
+
+ // 4. run the test-suite
+ yield validate();
+ do_print("validated import");
+});
+
+function* validate() {
+ yield testMenuBookmarks();
+ yield testToolbarBookmarks();
+ testUnfiledBookmarks();
+ testTags();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+}
+
+// Tests a bookmarks datastore that has a set of bookmarks, etc
+// that flex each supported field and feature.
+function* testMenuBookmarks() {
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).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 = yield PlacesUtils.bookmarks.fetch(folderNode.bookmarkGuid);
+ Assert.equal(folder.dateAdded.getTime(), 1177541020000);
+
+ Assert.equal(PlacesUtils.asQuery(folderNode).hasChildren, true);
+
+ Assert.equal("folder test comment",
+ PlacesUtils.annotations.getItemAnnotation(folderNode.itemId,
+ DESCRIPTION_ANNO));
+
+ // 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.ok(PlacesUtils.annotations.itemHasAnnotation(bookmarkNode.itemId,
+ LOAD_IN_SIDEBAR_ANNO));
+ Assert.equal(bookmarkNode.dateAdded, 1177375336000000);
+
+ let entry = yield PlacesUtils.keywords.fetch({ url: bookmarkNode.uri });
+ Assert.equal("test", entry.keyword);
+ Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData);
+
+ Assert.equal("ISO-8859-1",
+ (yield PlacesUtils.getCharsetForURI(NetUtil.newURI(bookmarkNode.uri))));
+ Assert.equal("item description",
+ PlacesUtils.annotations.getItemAnnotation(bookmarkNode.itemId,
+ DESCRIPTION_ANNO));
+
+ folderNode.containerOpen = false;
+ root.containerOpen = false;
+}
+
+function* testToolbarBookmarks() {
+ let root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+
+ // child count (add 2 for pre-existing items)
+ Assert.equal(root.childCount, bookmarkData.length + 2);
+
+ let livemarkNode = root.getChild(1);
+ Assert.equal("Latest Headlines", livemarkNode.title);
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ id: livemarkNode.itemId });
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ livemark.siteURI.spec);
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+ livemark.feedURI.spec);
+
+ // 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.unfiledBookmarksFolderId).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) {
+ do_print("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 000000000..4b60d4768
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_385397.js
@@ -0,0 +1,142 @@
+/* -*- 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;
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(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);
+ yield 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.
+ do_check_eq(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/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ do_check_eq(node.uri, site + "blank.gif");
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ do_check_eq(node.uri, site);
+ do_check_eq(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
+ do_check_eq(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/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ node = root.getChild(++index);
+ do_check_eq(node.uri, site);
+ do_check_eq(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;
+ do_check_eq(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/";
+ do_check_eq(node.uri, site);
+ do_check_eq(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;
+ do_check_eq(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/";
+ do_check_eq(node.uri, site);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ }
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_399264_query_to_string.js b/toolkit/components/places/tests/unit/test_399264_query_to_string.js
new file mode 100644
index 000000000..6e6cc279c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_399264_query_to_string.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/. */
+
+/**
+ * Obtains the id of the folder obtained from the query.
+ *
+ * @param aFolderID
+ * The id of the folder we want to generate a query for.
+ * @returns the string representation of the query for the given folder.
+ */
+function query_string(aFolderID)
+{
+ var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+ var query = hs.getNewQuery();
+ query.setFolders([aFolderID], 1);
+ var options = hs.getNewQueryOptions();
+ return hs.queriesToQueryString([query], 1, options);
+}
+
+function run_test()
+{
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+ const QUERIES = [
+ "folder=PLACES_ROOT"
+ , "folder=BOOKMARKS_MENU"
+ , "folder=TAGS"
+ , "folder=UNFILED_BOOKMARKS"
+ , "folder=TOOLBAR"
+ ];
+ const FOLDER_IDS = [
+ bs.placesRoot
+ , bs.bookmarksMenuFolder
+ , bs.tagsFolder
+ , bs.unfiledBookmarksFolder
+ , bs.toolbarFolder
+ ];
+
+
+ for (var i = 0; i < QUERIES.length; i++) {
+ var result = query_string(FOLDER_IDS[i]);
+ dump("Looking for '" + QUERIES[i] + "' in '" + result + "'\n");
+ do_check_neq(-1, result.indexOf(QUERIES[i]));
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_399264_string_to_query.js b/toolkit/components/places/tests/unit/test_399264_string_to_query.js
new file mode 100644
index 000000000..bd29316d9
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_399264_string_to_query.js
@@ -0,0 +1,75 @@
+/* -*- 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/. */
+
+/**
+ * Obtains the id of the folder obtained from the query.
+ *
+ * @param aQuery
+ * The query to obtain the folder id from.
+ * @returns the folder id of the folder of the root node of the query.
+ */
+function folder_id(aQuery)
+{
+ var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+ dump("Checking query '" + aQuery + "'\n");
+ var options = { };
+ var queries = { };
+ var size = { };
+ hs.queryStringToQueries(aQuery, queries, size, options);
+ var result = hs.executeQueries(queries.value, size.value, options.value);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_true(root.hasChildren);
+ var folderID = root.getChild(0).parent.itemId;
+ root.containerOpen = false;
+ return folderID;
+}
+
+function run_test()
+{
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+ const QUERIES = [
+ "place:folder=PLACES_ROOT"
+ , "place:folder=BOOKMARKS_MENU"
+ , "place:folder=TAGS"
+ , "place:folder=UNFILED_BOOKMARKS"
+ , "place:folder=TOOLBAR"
+ ];
+ const FOLDER_IDS = [
+ bs.placesRoot
+ , bs.bookmarksMenuFolder
+ , bs.tagsFolder
+ , bs.unfiledBookmarksFolder
+ , bs.toolbarFolder
+ ];
+
+ // add something in the bookmarks menu folder so a query to it returns results
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://example.com/bmf/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "bmf");
+
+ // add something to the tags folder
+ var ts = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+ ts.tagURI(uri("http://www.example.com/"), ["tag"]);
+
+ // add something to the unfiled bookmarks folder
+ bs.insertBookmark(bs.unfiledBookmarksFolder, uri("http://example.com/ubf/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "ubf");
+
+ // add something to the toolbar folder
+ bs.insertBookmark(bs.toolbarFolder, uri("http://example.com/tf/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "tf");
+
+ for (var i = 0; i < QUERIES.length; i++) {
+ var result = folder_id(QUERIES[i]);
+ dump("expected " + FOLDER_IDS[i] + ", got " + result + "\n");
+ do_check_eq(FOLDER_IDS[i], result);
+ }
+}
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 000000000..296d69414
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_399266.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/. */
+
+const TOTAL_SITES = 20;
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(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 });
+ }
+ }
+ yield 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;
+ do_check_eq(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/";
+ do_check_eq(node.uri, site);
+ do_check_eq(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;
+ do_check_eq(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/";
+ do_check_eq(node.uri, site);
+ do_check_eq(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 000000000..263e20aa5
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_402799.js
@@ -0,0 +1,62 @@
+/* -*- 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);
+ var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory);
+} catch (ex) {
+ do_throw("Could not get history services\n");
+}
+
+// Get bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+}
+catch (ex) {
+ do_throw("Could not get the nav-bookmarks-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");
+}
+
+
+// main
+function run_test() {
+ var uri1 = uri("http://foo.bar/");
+
+ // create 2 bookmarks on the same uri
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri1,
+ bmsvc.DEFAULT_INDEX, "title 1");
+ bmsvc.insertBookmark(bmsvc.toolbarFolder, uri1,
+ bmsvc.DEFAULT_INDEX, "title 2");
+ // add some tags
+ tagssvc.tagURI(uri1, ["foo", "bar", "foobar", "foo bar"]);
+
+ // check that a generic bookmark query returns only real bookmarks
+ var options = histsvc.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ var query = histsvc.getNewQuery();
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+
+ root.containerOpen = true;
+ var cc = root.childCount;
+ do_check_eq(cc, 2);
+ var node1 = root.getChild(0);
+ do_check_eq(bmsvc.getFolderIdForItem(node1.itemId), bmsvc.bookmarksMenuFolder);
+ var node2 = root.getChild(1);
+ do_check_eq(bmsvc.getFolderIdForItem(node2.itemId), bmsvc.toolbarFolder);
+ root.containerOpen = false;
+}
diff --git a/toolkit/components/places/tests/unit/test_405497.js b/toolkit/components/places/tests/unit/test_405497.js
new file mode 100644
index 000000000..951302b84
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_405497.js
@@ -0,0 +1,57 @@
+/* -*- 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+/**
+ * The callback object for runInBatchMode.
+ *
+ * @param aService
+ * Takes a reference to the history service or the bookmark service.
+ * This determines which service should be called when calling the second
+ * runInBatchMode the second time.
+ */
+function callback(aService)
+{
+ this.callCount = 0;
+ this.service = aService;
+}
+callback.prototype = {
+ // nsINavHistoryBatchCallback
+
+ runBatched: function(aUserData)
+ {
+ this.callCount++;
+
+ if (this.callCount == 1) {
+ // We want to call run in batched once more.
+ this.service.runInBatchMode(this, null);
+ return;
+ }
+
+ do_check_eq(this.callCount, 2);
+ do_test_finished();
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryBatchCallback])
+};
+
+function run_test() {
+ // checking the history service
+ do_test_pending();
+ hs.runInBatchMode(new callback(hs), null);
+
+ // checking the bookmark service
+ do_test_pending();
+ bs.runInBatchMode(new callback(bs), null);
+}
diff --git a/toolkit/components/places/tests/unit/test_408221.js b/toolkit/components/places/tests/unit/test_408221.js
new file mode 100644
index 000000000..2b41ce1a2
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_408221.js
@@ -0,0 +1,165 @@
+/* -*- 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 current_test = 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: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// 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");
+}
+
+function ensure_tag_results(uris, searchTerm)
+{
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, uris.length);
+ let vals = [];
+ for (let i=0; i<controller.matchCount; i++) {
+ // Keep the URL for later because order of tag results is undefined
+ vals.push(controller.getValueAt(i));
+ do_check_eq(controller.getStyleAt(i), "bookmark-tag");
+ }
+ // Sort the results then check if we have the right items
+ vals.sort().forEach((val, i) => do_check_eq(val, uris[i].spec))
+
+ if (current_test < (tests.length - 1)) {
+ current_test++;
+ tests[current_test]();
+ }
+
+ do_test_finished();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+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");
+
+var tests = [function() { ensure_tag_results([uri1, uri2, uri3], "foo"); },
+ function() { ensure_tag_results([uri1, uri2, uri3], "Foo"); },
+ function() { ensure_tag_results([uri1, uri2, uri3], "foO"); },
+ function() { ensure_tag_results([uri4, uri5, uri6], "bar mud"); },
+ function() { ensure_tag_results([uri4, uri5, uri6], "BAR MUD"); },
+ function() { ensure_tag_results([uri4, uri5, uri6], "Bar Mud"); }];
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param aURI
+ * The nsIURI to tag.
+ * @param aTags
+ * The tags to add.
+ */
+function tagURI(aURI, aTags) {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A title");
+ tagssvc.tagURI(aURI, aTags);
+}
+
+/**
+ * Test bug #408221
+ */
+function run_test() {
+ // always search in history + bookmarks, no matter what the default is
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setIntPref("browser.urlbar.search.sources", 3);
+ prefs.setIntPref("browser.urlbar.default.behavior", 0);
+
+ tagURI(uri1, ["Foo"]);
+ tagURI(uri2, ["FOO"]);
+ tagURI(uri3, ["foO"]);
+ tagURI(uri4, ["BAR"]);
+ tagURI(uri4, ["MUD"]);
+ tagURI(uri5, ["bar"]);
+ tagURI(uri5, ["mud"]);
+ tagURI(uri6, ["baR"]);
+ tagURI(uri6, ["muD"]);
+
+ tests[0]();
+}
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 000000000..827391f18
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_412132.js
@@ -0,0 +1,136 @@
+/* -*- 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
+ */
+
+add_task(function* changeuri_unvisited_bookmark()
+{
+ do_print("After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should be zero if original URI is unvisited and " +
+ "no longer bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ PlacesUtils.bookmarks.changeBookmarkURI(id, uri("http://example.com/2"));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Unvisited URI no longer bookmarked => frecency should = 0");
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* changeuri_visited_bookmark()
+{
+ do_print("After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should not be zero if original URI is visited.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ PlacesUtils.bookmarks.changeBookmarkURI(id, uri("http://example.com/2"));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("*Visited* URI no longer bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* changeuri_bookmark_still_bookmarked()
+{
+ do_print("After changing URI of bookmark, frecency of bookmark's " +
+ "original URI should not be zero if original URI is still " +
+ "bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let id1 = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark 1 title");
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark 2 title");
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ PlacesUtils.bookmarks.changeBookmarkURI(id1, uri("http://example.com/2"));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("URI still bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* changeuri_nonexistent_bookmark()
+{
+ do_print("Changing the URI of a nonexistent bookmark should fail.");
+ function tryChange(itemId)
+ {
+ try {
+ PlacesUtils.bookmarks.changeBookmarkURI(itemId + 1, uri("http://example.com/2"));
+ do_throw("Nonexistent bookmark should throw.");
+ }
+ catch (ex) {}
+ }
+
+ // First try a straight-up bogus item ID, one greater than the current max
+ // ID.
+ let stmt = DBConn().createStatement("SELECT MAX(id) FROM moz_bookmarks");
+ stmt.executeStep();
+ let maxId = stmt.getInt32(0);
+ stmt.finalize();
+ tryChange(maxId + 1);
+
+ // Now add a bookmark, delete it, and check.
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://example.com/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+ PlacesUtils.bookmarks.removeItem(id);
+ tryChange(id);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_413784.js b/toolkit/components/places/tests/unit/test_413784.js
new file mode 100644
index 000000000..6df4dfbbb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_413784.js
@@ -0,0 +1,118 @@
+/* -*- 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 autocomplete for non-English URLs
+
+- add a visit for a page with a non-English URL
+- search
+- test number of matches (should be exactly one)
+
+*/
+
+var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+// create test data
+var searchTerm = "ユニコード";
+var decoded = "http://www.foobar.com/" + searchTerm + "/";
+var url = uri(decoded);
+
+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: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+function run_test()
+{
+ do_test_pending();
+ PlacesTestUtils.addVisits(url).then(continue_test);
+}
+
+function continue_test()
+{
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+
+ // test that we found the entry we added
+ do_check_eq(controller.matchCount, 1);
+
+ // Make sure the url is the same according to spec, so it can be deleted
+ do_check_eq(controller.getValueAt(0), url.spec);
+
+ do_test_finished();
+ };
+
+ controller.startSearch(searchTerm);
+}
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 000000000..f2e049f09
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_415460.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/. */
+
+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);
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ const SEARCH_TERM = "ユニコード";
+ const TEST_URL = "http://example.com/" + SEARCH_TERM + "/";
+ yield PlacesTestUtils.addVisits(uri(TEST_URL));
+ do_check_true(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 000000000..afd396183
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_415757.js
@@ -0,0 +1,102 @@
+/* -*- 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
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // add pages to global history
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test-" + i + ".com/";
+ let testURI = uri(site);
+ let when = Date.now() * 1000 + (i * TOTAL_SITES);
+ yield PlacesTestUtils.addVisits({ uri: testURI, visitDate: when });
+ }
+ for (let i = 0; i < TOTAL_SITES; i++) {
+ let site = "http://www.test.com/" + i + "/";
+ let testURI = uri(site);
+ let when = Date.now() * 1000 + (i * TOTAL_SITES);
+ yield PlacesTestUtils.addVisits({ uri: testURI, visitDate: when });
+ }
+
+ // set a page annotation on one of the urls that will be removed
+ var testAnnoDeletedURI = uri("http://www.test.com/1/");
+ var testAnnoDeletedName = "foo";
+ var testAnnoDeletedValue = "bar";
+ PlacesUtils.annotations.setPageAnnotation(testAnnoDeletedURI,
+ testAnnoDeletedName,
+ testAnnoDeletedValue, 0,
+ PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+
+ // set a page annotation on one of the urls that will NOT be removed
+ var testAnnoRetainedURI = uri("http://www.test-1.com/");
+ var testAnnoRetainedName = "foo";
+ var testAnnoRetainedValue = "bar";
+ PlacesUtils.annotations.setPageAnnotation(testAnnoRetainedURI,
+ testAnnoRetainedName,
+ testAnnoRetainedValue, 0,
+ PlacesUtils.annotations.EXPIRE_WITH_HISTORY);
+
+ // remove pages from www.test.com
+ PlacesUtils.history.removePagesFromHost("www.test.com", false);
+
+ // 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);
+ do_check_false(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);
+ do_check_true(uri_in_db(testURI));
+ }
+
+ // check that annotation on the removed item does not exists
+ try {
+ PlacesUtils.annotations.getPageAnnotation(testAnnoDeletedURI, testAnnoName);
+ do_throw("fetching page-annotation that doesn't exist, should've thrown");
+ } catch (ex) {}
+
+ // check that annotation on the NOT removed item still exists
+ try {
+ var annoVal = PlacesUtils.annotations.getPageAnnotation(testAnnoRetainedURI,
+ testAnnoRetainedName);
+ } catch (ex) {
+ do_throw("The annotation has been removed erroneously");
+ }
+ do_check_eq(annoVal, testAnnoRetainedValue);
+
+});
diff --git a/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js b/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js
new file mode 100644
index 000000000..2eed02921
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_418643_removeFolderChildren.js
@@ -0,0 +1,143 @@
+/* -*- 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.
+try {
+ var histSvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmSvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ var annoSvc = Cc["@mozilla.org/browser/annotation-service;1"]
+ .getService(Ci.nsIAnnotationService);
+} catch (ex) {
+ do_throw("Could not get services\n");
+}
+
+var validAnnoName = "validAnno";
+var validItemName = "validItem";
+var deletedAnnoName = "deletedAnno";
+var deletedItemName = "deletedItem";
+var bookmarkedURI = uri("http://www.mozilla.org/");
+// set lastModified to the past to prevent VM timing bugs
+var pastDate = Date.now() * 1000 - 1;
+var deletedBookmarkIds = [];
+
+// bookmarks observer
+var observer = {
+ // cached ordered array of notified items
+ _onItemRemovedItemIds: [],
+ onItemRemoved: function(aItemId, aParentId, aIndex) {
+ // We should first get notifications for children, then for their parent
+ do_check_eq(this._onItemRemovedItemIds.indexOf(aParentId), -1);
+ // Ensure we are not wrongly removing 1 level up
+ do_check_neq(aParentId, bmSvc.toolbarFolder);
+ // Removed item must be one of those we have manually deleted
+ do_check_neq(deletedBookmarkIds.indexOf(aItemId), -1);
+ this._onItemRemovedItemIds.push(aItemId);
+ },
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsINavBookmarkObserver) ||
+ aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+bmSvc.addObserver(observer, false);
+
+function add_bookmarks() {
+ // This is the folder we will cleanup
+ var validFolderId = bmSvc.createFolder(bmSvc.toolbarFolder,
+ validItemName,
+ bmSvc.DEFAULT_INDEX);
+ annoSvc.setItemAnnotation(validFolderId, validAnnoName,
+ "annotation", 0,
+ annoSvc.EXPIRE_NEVER);
+ bmSvc.setItemLastModified(validFolderId, pastDate);
+
+ // This bookmark should not be deleted
+ var validItemId = bmSvc.insertBookmark(bmSvc.toolbarFolder,
+ bookmarkedURI,
+ bmSvc.DEFAULT_INDEX,
+ validItemName);
+ annoSvc.setItemAnnotation(validItemId, validAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+
+ // The following contents should be deleted
+ var deletedItemId = bmSvc.insertBookmark(validFolderId,
+ bookmarkedURI,
+ bmSvc.DEFAULT_INDEX,
+ deletedItemName);
+ annoSvc.setItemAnnotation(deletedItemId, deletedAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+ deletedBookmarkIds.push(deletedItemId);
+
+ var internalFolderId = bmSvc.createFolder(validFolderId,
+ deletedItemName,
+ bmSvc.DEFAULT_INDEX);
+ annoSvc.setItemAnnotation(internalFolderId, deletedAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+ deletedBookmarkIds.push(internalFolderId);
+
+ deletedItemId = bmSvc.insertBookmark(internalFolderId,
+ bookmarkedURI,
+ bmSvc.DEFAULT_INDEX,
+ deletedItemName);
+ annoSvc.setItemAnnotation(deletedItemId, deletedAnnoName,
+ "annotation", 0, annoSvc.EXPIRE_NEVER);
+ deletedBookmarkIds.push(deletedItemId);
+
+ return validFolderId;
+}
+
+function check_bookmarks(aFolderId) {
+ // check that we still have valid bookmarks
+ var bookmarks = bmSvc.getBookmarkIdsForURI(bookmarkedURI);
+ for (var i = 0; i < bookmarks.length; i++) {
+ do_check_eq(bmSvc.getItemTitle(bookmarks[i]), validItemName);
+ do_check_true(annoSvc.itemHasAnnotation(bookmarks[i], validAnnoName));
+ }
+
+ // check that folder exists and has still its annotation
+ do_check_eq(bmSvc.getItemTitle(aFolderId), validItemName);
+ do_check_true(annoSvc.itemHasAnnotation(aFolderId, validAnnoName));
+
+ // check that folder is empty
+ var options = histSvc.getNewQueryOptions();
+ var query = histSvc.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ var result = histSvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ // test that lastModified got updated
+ do_check_true(pastDate < bmSvc.getItemLastModified(aFolderId));
+
+ // test that all children have been deleted, we use annos for that
+ var deletedItems = annoSvc.getItemsWithAnnotation(deletedAnnoName);
+ do_check_eq(deletedItems.length, 0);
+
+ // test that observer has been called for (and only for) deleted items
+ do_check_eq(observer._onItemRemovedItemIds.length, deletedBookmarkIds.length);
+
+ // Sanity check: all roots should be intact
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.placesRoot), 0);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.bookmarksMenuFolder), bmSvc.placesRoot);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.tagsFolder), bmSvc.placesRoot);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.unfiledBookmarksFolder), bmSvc.placesRoot);
+ do_check_eq(bmSvc.getFolderIdForItem(bmSvc.toolbarFolder), bmSvc.placesRoot);
+}
+
+// main
+function run_test() {
+ var folderId = add_bookmarks();
+ bmSvc.removeFolderChildren(folderId);
+ check_bookmarks(folderId);
+}
diff --git a/toolkit/components/places/tests/unit/test_419731.js b/toolkit/components/places/tests/unit/test_419731.js
new file mode 100644
index 000000000..b1a434e12
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_419731.js
@@ -0,0 +1,96 @@
+/* -*- 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 uri1 = NetUtil.newURI("http://foo.bar/");
+
+ // create 2 bookmarks
+ let bookmark1id = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "title 1");
+ let bookmark2id = PlacesUtils.bookmarks
+ .insertBookmark(PlacesUtils.toolbarFolderId,
+ uri1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "title 2");
+ // add a new tag
+ PlacesUtils.tagging.tagURI(uri1, ["foo"]);
+
+ // get tag folder id
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.tagsFolderId], 1);
+ let result = PlacesUtils.history.executeQuery(query, options);
+ let tagRoot = result.root;
+ tagRoot.containerOpen = true;
+ let tagNode = tagRoot.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ let tagItemId = tagNode.itemId;
+ tagRoot.containerOpen = false;
+
+ // change bookmark 1 title
+ PlacesUtils.bookmarks.setItemTitle(bookmark1id, "new title 1");
+
+ // Workaround timers resolution and time skews.
+ let bookmark2LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark2id);
+ PlacesUtils.bookmarks.setItemLastModified(bookmark1id, bookmark2LastMod + 1000);
+
+ // Query the tag.
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.resultType = options.RESULTS_AS_TAG_QUERY;
+
+ query = PlacesUtils.history.getNewQuery();
+ result = PlacesUtils.history.executeQuery(query, options);
+ let root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+
+ let theTag = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ // Bug 524219: Check that renaming the tag shows up in the result.
+ do_check_eq(theTag.title, "foo")
+ PlacesUtils.bookmarks.setItemTitle(tagItemId, "bar");
+
+ // Check that the item has been replaced
+ do_check_neq(theTag, root.getChild(0));
+ theTag = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(theTag.title, "bar");
+
+ // Check that tag container contains new title
+ theTag.containerOpen = true;
+ do_check_eq(theTag.childCount, 1);
+ let node = theTag.getChild(0);
+ do_check_eq(node.title, "new title 1");
+ theTag.containerOpen = false;
+ root.containerOpen = false;
+
+ // Change bookmark 2 title.
+ PlacesUtils.bookmarks.setItemTitle(bookmark2id, "new title 2");
+
+ // Workaround timers resolution and time skews.
+ let bookmark1LastMod = PlacesUtils.bookmarks.getItemLastModified(bookmark1id);
+ PlacesUtils.bookmarks.setItemLastModified(bookmark2id, bookmark1LastMod + 1000);
+
+ // Check that tag container contains new title
+ options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.resultType = options.RESULTS_AS_TAG_CONTENTS;
+
+ query = PlacesUtils.history.getNewQuery();
+ query.setFolders([tagItemId], 1);
+ result = PlacesUtils.history.executeQuery(query, options);
+ root = result.root;
+
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+ node = root.getChild(0);
+ do_check_eq(node.title, "new title 2");
+ root.containerOpen = false;
+}
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 000000000..4c726d667
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_419792_node_tags_property.js
@@ -0,0 +1,49 @@
+/* -*- 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 bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+
+function run_test() {
+ // get toolbar node
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.toolbarFolder], 1);
+ var result = histsvc.executeQuery(query, options);
+ var toolbarNode = result.root;
+ toolbarNode.containerOpen = true;
+
+ // add a bookmark
+ var bookmarkURI = uri("http://foo.com");
+ var bookmarkId = bmsvc.insertBookmark(bmsvc.toolbarFolder, bookmarkURI,
+ bmsvc.DEFAULT_INDEX, "");
+
+ // get the node for the new bookmark
+ var node = toolbarNode.getChild(toolbarNode.childCount-1);
+ do_check_eq(node.itemId, bookmarkId);
+
+ // confirm there's no tags via the .tags property
+ do_check_eq(node.tags, null);
+
+ // add a tag
+ tagssvc.tagURI(bookmarkURI, ["foo"]);
+ do_check_eq(node.tags, "foo");
+
+ // add another tag, to test delimiter and sorting
+ tagssvc.tagURI(bookmarkURI, ["bar"]);
+ do_check_eq(node.tags, "bar, foo");
+
+ // remove the tags, confirming the property is cleared
+ tagssvc.untagURI(bookmarkURI, null);
+ do_check_eq(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 000000000..bee3a4a54
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_425563.js
@@ -0,0 +1,74 @@
+/* -*- 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()
+{
+ run_next_test();
+}
+
+add_task(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-embed.com/",
+ "http://www.test-download.com/",
+ "http://www.test-framed.com/",
+ "http://www.test-reload.com/"];
+
+ // add visits, one for each transition type
+ yield 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-embed.com/"),
+ transition: TRANSITION_EMBED },
+ { 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) {
+ do_check_true(yield promiseIsURIVisited(uri(visited_uri)));
+ }
+ for (let visited_uri of notcount_visited_URIs) {
+ do_check_true(yield promiseIsURIVisited(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;
+ do_check_eq(cc, count_visited_URIs.length);
+
+ for (let i = 0; i < cc; i++) {
+ let node = root.getChild(i);
+ do_check_neq(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 000000000..e0b6be64c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_429505_remove_shortcuts.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/. */
+
+/*
+
+- 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
+
+*/
+
+function run_test() {
+ const IDX = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ var folderId =
+ PlacesUtils.bookmarks.createFolder(PlacesUtils.toolbarFolderId, "", IDX);
+
+ var queryId =
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.toolbarFolderId,
+ uri("place:folder=" + folderId), IDX, "");
+
+ var root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId, false, true).root;
+
+ var oldCount = root.childCount;
+
+ PlacesUtils.bookmarks.removeItem(queryId);
+
+ do_check_eq(root.childCount, oldCount-1);
+
+ root.containerOpen = false;
+}
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 000000000..52558e844
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_433317_query_title_update.js
@@ -0,0 +1,38 @@
+/* -*- 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() {
+ try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ } catch (ex) {
+ do_throw("Unable to initialize Places services");
+ }
+
+ // create a query bookmark
+ var queryId = bmsvc.insertBookmark(bmsvc.toolbarFolder, uri("place:"),
+ 0 /* first item */, "test query");
+
+ // query for that query
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.toolbarFolder], 1);
+ var result = histsvc.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var queryNode = root.getChild(0);
+ do_check_eq(queryNode.title, "test query");
+
+ // change the title
+ bmsvc.setItemTitle(queryId, "foo");
+
+ // confirm the node was updated
+ do_check_eq(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 000000000..92dac0b17
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_433525_hasChildren_crash.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/. */
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ } catch (ex) {
+ do_throw("Unable to initialize Places services");
+ }
+
+ // add a visit
+ var testURI = uri("http://test");
+ yield 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
+ do_check_eq(root.hasChildren, true);
+
+ // now check via the saved search path
+ var queryURI = histsvc.queriesToQueryString([query], 1, options);
+ bmsvc.insertBookmark(bmsvc.toolbarFolder, uri(queryURI),
+ 0 /* first item */, "test query");
+
+ // query for that query
+ options = histsvc.getNewQueryOptions();
+ query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.toolbarFolder], 1);
+ result = histsvc.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ var queryNode = root.getChild(0);
+ do_check_eq(queryNode.title, "test query");
+ queryNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(queryNode.hasChildren, true);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_452777.js b/toolkit/components/places/tests/unit/test_452777.js
new file mode 100644
index 000000000..97b2852f6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_452777.js
@@ -0,0 +1,36 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim:set ts=2 sw=2 sts=2 expandtab
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This test ensures that when removing a folder within a transaction, undoing
+ * the transaction restores it with the same id (as received by the observers).
+ */
+
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+function run_test()
+{
+ const TITLE = "test folder";
+
+ // Create two test folders; remove the first one. This ensures that undoing
+ // the removal will not get the same id by chance (the insert id's can be
+ // reused in SQLite).
+ let id = bs.createFolder(bs.placesRoot, TITLE, -1);
+ bs.createFolder(bs.placesRoot, "test folder 2", -1);
+ let transaction = bs.getRemoveFolderTransaction(id);
+ transaction.doTransaction();
+
+ // Now check to make sure it gets added with the right id
+ bs.addObserver({
+ onItemAdded: function(aItemId, aFolder, aIndex, aItemType, aURI, aTitle)
+ {
+ do_check_eq(aItemId, id);
+ do_check_eq(aTitle, TITLE);
+ }
+ }, false);
+ transaction.undoTransaction();
+}
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 000000000..606e83048
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_454977.js
@@ -0,0 +1,124 @@
+/* -*- 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.
+function* task_add_visit(aURI, aVisitType)
+{
+ // Add the visit asynchronously, and save its visit ID.
+ let deferUpdatePlaces = new Promise((resolve, reject) =>
+ {
+ PlacesUtils.asyncHistory.updatePlaces({
+ uri: aURI,
+ visits: [{ transitionType: aVisitType, visitDate: Date.now() * 1000 }]
+ }, {
+ handleError: function TAV_handleError() {
+ reject(new Error("Unexpected error in adding visit."));
+ },
+ handleResult: function (aPlaceInfo) {
+ this.visitId = aPlaceInfo.visits[0].visitId;
+ },
+ handleCompletion: function TAV_handleCompletion() {
+ resolve(this.visitId);
+ }
+ });
+ });
+
+ let visitId = yield deferUpdatePlaces;
+
+ // 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);
+ do_check_true(stmt.executeStep());
+ let placeId = stmt.getInt64(0);
+ stmt.finalize();
+ do_check_true(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
+ do_check_eq(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
+ do_check_eq(root.childCount, aExpectedCountWithHidden);
+ root.containerOpen = false;
+}
+
+// main
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ const TEST_URI = uri("http://test.mozilla.org/");
+
+ // Add a visit that force hidden
+ yield task_add_visit(TEST_URI, TRANSITION_EMBED);
+ check_results(0, 0);
+
+ let placeId = yield 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.
+ do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_TYPED)), placeId);
+ check_results(1, 1);
+
+ // Add a visit that should not increase visit_count
+ do_check_eq((yield task_add_visit(TEST_URI, TRANSITION_RELOAD)), placeId);
+ check_results(1, 1);
+
+ // Add a visit that should not increase visit_count
+ do_check_eq((yield 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.
+ yield 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 000000000..2f7cece4a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_463863.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/. */
+
+/*
+ * 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;
+ do_check_eq(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
+ do_check_neq(node.uri.substr(6, 1), TRANSITION_EMBED);
+ do_check_neq(node.uri.substr(6, 1), TRANSITION_FRAMED_LINK);
+ }
+ root.containerOpen = false;
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // add visits, one for each transition type
+ for (let transition of transitions) {
+ yield PlacesTestUtils.addVisits({
+ uri: uri("http://" + transition + ".mozilla.org/"),
+ transition: 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 000000000..873174ffd
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_485442_crash_bug_nsNavHistoryQuery_GetUri.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/. */
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+function run_test() {
+ var query = hs.getNewQuery();
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULT_TYPE_QUERY;
+ var result = hs.executeQuery(query, options);
+ result.root.containerOpen = true;
+ var rootNode = result.root;
+ rootNode.QueryInterface(Ci.nsINavHistoryQueryResultNode);
+ var queries = rootNode.getQueries();
+ do_check_eq(queries[0].uri, null); // Should be null, instead of crashing the browser
+ rootNode.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 000000000..05f3f83e7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_486978_sort_by_date_queries.js
@@ -0,0 +1,129 @@
+/* -*- 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/",
+];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_initialize()
+{
+ var noon = new Date();
+ noon.setHours(12);
+
+ // Add visits.
+ for (let pageIndex = 0; pageIndex < pages.length; ++pageIndex) {
+ let page = pages[pageIndex];
+ yield PlacesTestUtils.addVisits({
+ uri: uri(page),
+ visitDate: noon - (pages.length - pageIndex) * 1000
+ });
+ }
+});
+
+/**
+ * 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;
+ do_check_eq(cc, pages.length);
+ for (var i = 0; i < cc; i++) {
+ var node = dayContainer.getChild(i);
+ do_check_eq(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;
+ do_check_eq(cc, pages.length);
+ for (var i = 0; i < cc; i++) {
+ var node = dayContainer.getChild(i);
+ do_check_eq(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);
+ do_check_eq(siteContainer.title, "a.mozilla.org");
+ siteContainer.containerOpen = true;
+
+ var cc = siteContainer.childCount;
+ do_check_eq(cc, pages.length / 2);
+ for (var i = 0; i < cc / 2; i++) {
+ var node = siteContainer.getChild(i);
+ do_check_eq(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 000000000..b61b91866
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_536081.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/. */
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bh = hs.QueryInterface(Ci.nsIBrowserHistory);
+var db = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+
+const URLS = [
+ { 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" },
+];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ for (let url of URLS) {
+ yield task_test_url(url);
+ }
+});
+
+function* task_test_url(aURL) {
+ print("Testing url: " + aURL.u);
+ yield PlacesTestUtils.addVisits(uri(aURL.u));
+ let query = hs.getNewQuery();
+ query.searchTerms = aURL.s;
+ let options = hs.getNewQueryOptions();
+ let root = hs.executeQuery(query, options).root;
+ root.containerOpen = true;
+ let cc = root.childCount;
+ do_check_eq(cc, 1);
+ print("Checking url is in the query.");
+ let node = root.getChild(0);
+ print("Found " + node.uri);
+ root.containerOpen = false;
+ bh.removePage(uri(node.uri));
+}
+
+function check_empty_table(table_name) {
+ print("Checking url has been removed.");
+ let stmt = db.createStatement("SELECT count(*) FROM " + table_name);
+ try {
+ stmt.executeStep();
+ do_check_eq(stmt.getInt32(0), 0);
+ }
+ finally {
+ stmt.finalize();
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js
new file mode 100644
index 000000000..1280ce3e7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesSearchAutocompleteProvider.js
@@ -0,0 +1,133 @@
+/* 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/. */
+
+Cu.import("resource://gre/modules/PlacesSearchAutocompleteProvider.jsm");
+
+function run_test() {
+ // Tell the search service we are running in the US. This also has the
+ // desired side-effect of preventing our geoip lookup.
+ Services.prefs.setBoolPref("browser.search.isUS", true);
+ Services.prefs.setCharPref("browser.search.countryCode", "US");
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+ run_next_test();
+}
+
+add_task(function* search_engine_match() {
+ let engine = yield promiseDefaultSearchEngine();
+ let token = engine.getResultDomain();
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1));
+ do_check_eq(match.url, engine.searchForm);
+ do_check_eq(match.engineName, engine.name);
+ do_check_eq(match.iconUrl, engine.iconURI ? engine.iconURI.spec : null);
+});
+
+add_task(function* no_match() {
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("test"));
+});
+
+add_task(function* hide_search_engine_nomatch() {
+ let engine = yield promiseDefaultSearchEngine();
+ let token = engine.getResultDomain();
+ let promiseTopic = promiseSearchTopic("engine-changed");
+ Services.search.removeEngine(engine);
+ yield promiseTopic;
+ do_check_true(engine.hidden);
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken(token.substr(0, 1)));
+});
+
+add_task(function* add_search_engine_match() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
+ Services.search.addEngineWithDetails("bacon", "", "pork", "Search Bacon",
+ "GET", "http://www.bacon.moz/?search={searchTerms}");
+ yield promiseTopic;
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon");
+ do_check_eq(match.url, "http://www.bacon.moz");
+ do_check_eq(match.engineName, "bacon");
+ do_check_eq(match.iconUrl, null);
+});
+
+add_task(function* test_aliased_search_engine_match() {
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByAlias("sober"));
+ // Lower case
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias("pork");
+ do_check_eq(match.engineName, "bacon");
+ do_check_eq(match.alias, "pork");
+ do_check_eq(match.iconUrl, null);
+ // Upper case
+ let match1 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("PORK");
+ do_check_eq(match1.engineName, "bacon");
+ do_check_eq(match1.alias, "pork");
+ do_check_eq(match1.iconUrl, null);
+ // Cap case
+ let match2 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("Pork");
+ do_check_eq(match2.engineName, "bacon");
+ do_check_eq(match2.alias, "pork");
+ do_check_eq(match2.iconUrl, null);
+});
+
+add_task(function* test_aliased_search_engine_match_upper_case_alias() {
+ let promiseTopic = promiseSearchTopic("engine-added");
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("patch"));
+ Services.search.addEngineWithDetails("patch", "", "PR", "Search Patch",
+ "GET", "http://www.patch.moz/?search={searchTerms}");
+ yield promiseTopic;
+ // lower case
+ let match = yield PlacesSearchAutocompleteProvider.findMatchByAlias("pr");
+ do_check_eq(match.engineName, "patch");
+ do_check_eq(match.alias, "PR");
+ do_check_eq(match.iconUrl, null);
+ // Upper case
+ let match1 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("PR");
+ do_check_eq(match1.engineName, "patch");
+ do_check_eq(match1.alias, "PR");
+ do_check_eq(match1.iconUrl, null);
+ // Cap case
+ let match2 = yield PlacesSearchAutocompleteProvider.findMatchByAlias("Pr");
+ do_check_eq(match2.engineName, "patch");
+ do_check_eq(match2.alias, "PR");
+ do_check_eq(match2.iconUrl, null);
+});
+
+add_task(function* remove_search_engine_nomatch() {
+ let engine = Services.search.getEngineByName("bacon");
+ let promiseTopic = promiseSearchTopic("engine-removed");
+ Services.search.removeEngine(engine);
+ yield promiseTopic;
+ do_check_eq(null, yield PlacesSearchAutocompleteProvider.findMatchByToken("bacon"));
+});
+
+add_task(function* test_parseSubmissionURL_basic() {
+ // Most of the logic of parseSubmissionURL is tested in the search service
+ // itself, thus we only do a sanity check of the wrapper here.
+ let engine = yield promiseDefaultSearchEngine();
+ let submissionURL = engine.getSubmission("terms").uri.spec;
+
+ let result = PlacesSearchAutocompleteProvider.parseSubmissionURL(submissionURL);
+ do_check_eq(result.engineName, engine.name);
+ do_check_eq(result.terms, "terms");
+
+ result = PlacesSearchAutocompleteProvider.parseSubmissionURL("http://example.org/");
+ do_check_eq(result, null);
+});
+
+function promiseDefaultSearchEngine() {
+ let deferred = Promise.defer();
+ Services.search.init( () => {
+ deferred.resolve(Services.search.defaultEngine);
+ });
+ return deferred.promise;
+}
+
+function promiseSearchTopic(expectedVerb) {
+ let deferred = Promise.defer();
+ Services.obs.addObserver( function observe(subject, topic, verb) {
+ do_print("browser-search-engine-modified: " + verb);
+ if (verb == expectedVerb) {
+ Services.obs.removeObserver(observe, "browser-search-engine-modified");
+ deferred.resolve();
+ }
+ }, "browser-search-engine-modified", false);
+ return deferred.promise;
+}
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js b/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js
new file mode 100644
index 000000000..182f75eac
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_asyncGetBookmarkIds.js
@@ -0,0 +1,77 @@
+/**
+ * This file tests PlacesUtils.asyncGetBookmarkIds method.
+ */
+
+const TEST_URL = "http://www.example.com/";
+
+var promiseAsyncGetBookmarkIds = Task.async(function* (url) {
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ return new Promise(resolve => {
+ PlacesUtils.asyncGetBookmarkIds(url, (itemIds, uri) => {
+ Assert.equal(uri, url);
+ resolve({ itemIds, url });
+ });
+ });
+});
+
+add_task(function* test_no_bookmark() {
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.equal(itemIds.length, 0);
+ Assert.equal(url, TEST_URL);
+});
+
+add_task(function* test_one_bookmark() {
+ let bookmark = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "test"
+ });
+ let itemId = yield PlacesUtils.promiseItemId(bookmark.guid);
+ {
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(NetUtil.newURI(TEST_URL));
+ Assert.equal(itemIds.length, 1);
+ Assert.equal(itemIds[0], itemId);
+ Assert.equal(url.spec, TEST_URL);
+ }
+ {
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.equal(itemIds.length, 1);
+ Assert.equal(itemIds[0], itemId);
+ Assert.equal(url, TEST_URL);
+ }
+ yield PlacesUtils.bookmarks.remove(bookmark);
+});
+
+add_task(function* test_multiple_bookmarks() {
+ let ids = [];
+ let bookmark1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "test"
+ });
+ ids.push((yield PlacesUtils.promiseItemId(bookmark1.guid)));
+ let bookmark2 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "test"
+ });
+ ids.push((yield PlacesUtils.promiseItemId(bookmark2.guid)));
+
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.deepEqual(ids, itemIds);
+ Assert.equal(url, TEST_URL);
+
+ yield PlacesUtils.bookmarks.remove(bookmark1);
+ yield PlacesUtils.bookmarks.remove(bookmark2);
+});
+
+add_task(function* test_cancel() {
+ let pending = PlacesUtils.asyncGetBookmarkIds(TEST_URL, () => {
+ Assert.ok(false, "A canceled pending statement should not be invoked");
+ });
+ pending.cancel();
+
+ let { itemIds, url } = yield promiseAsyncGetBookmarkIds(TEST_URL);
+ Assert.equal(itemIds.length, 0);
+ Assert.equal(url, TEST_URL);
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js
new file mode 100644
index 000000000..b7906ec5c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_invalidateCachedGuidFor.js
@@ -0,0 +1,25 @@
+add_task(function* () {
+ do_print("Add a bookmark.");
+ let bm = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let id = yield PlacesUtils.promiseItemId(bm.guid);
+ Assert.equal((yield PlacesUtils.promiseItemGuid(id)), bm.guid);
+
+ // Ensure invalidating a non-existent itemId doesn't throw.
+ PlacesUtils.invalidateCachedGuidFor(null);
+ PlacesUtils.invalidateCachedGuidFor(9999);
+
+ do_print("Change the GUID.");
+ yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) {
+ yield db.execute("UPDATE moz_bookmarks SET guid = :guid WHERE id = :id",
+ { guid: "123456789012", id});
+ }));
+ // The cache should still point to the wrong id.
+ Assert.equal((yield PlacesUtils.promiseItemGuid(id)), bm.guid);
+
+ do_print("Invalidate the cache.");
+ PlacesUtils.invalidateCachedGuidFor(id);
+ Assert.equal((yield PlacesUtils.promiseItemGuid(id)), "123456789012");
+ Assert.equal((yield PlacesUtils.promiseItemId("123456789012")), id);
+ yield Assert.rejects(PlacesUtils.promiseItemId(bm.guid), /no item found for the given GUID/);
+});
diff --git a/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js b/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js
new file mode 100644
index 000000000..f0e9c5517
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_PlacesUtils_lazyobservers.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ const TEST_URI = NetUtil.newURI("http://moz.org/")
+ let observer = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver,
+ ]),
+
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemAdded: function (aItemId, aParentId, aIndex, aItemType, aURI) {
+ do_check_true(aURI.equals(TEST_URI));
+ PlacesUtils.removeLazyBookmarkObserver(this);
+ do_test_finished();
+ },
+ onItemRemoved: function () {},
+ onItemChanged: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+ };
+
+ // Check registration and removal with uninitialized bookmarks service.
+ PlacesUtils.addLazyBookmarkObserver(observer);
+ PlacesUtils.removeLazyBookmarkObserver(observer);
+
+ // Add a proper lazy observer we will test.
+ PlacesUtils.addLazyBookmarkObserver(observer);
+
+ // Check that we don't leak when adding and removing an observer while the
+ // bookmarks service is instantiated but no change happened (bug 721319).
+ PlacesUtils.bookmarks;
+ PlacesUtils.addLazyBookmarkObserver(observer);
+ PlacesUtils.removeLazyBookmarkObserver(observer);
+ try {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ do_throw("Trying to remove a nonexisting observer should throw!");
+ } catch (ex) {}
+
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "Bookmark title");
+}
diff --git a/toolkit/components/places/tests/unit/test_adaptive.js b/toolkit/components/places/tests/unit/test_adaptive.js
new file mode 100644
index 000000000..78ffaedb5
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_adaptive.js
@@ -0,0 +1,406 @@
+/* -*- 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 395739 to make sure the feedback to the search results in those
+ * entries getting better ranks. Additionally, exact matches should be ranked
+ * higher. Because the interactions among adaptive rank and visit counts is not
+ * well defined, this test holds one of the two values constant when modifying
+ * the other.
+ *
+ * This also tests bug 395735 for the instrumentation feedback mechanism.
+ *
+ * Bug 411293 is tested to make sure the drop down strongly prefers previously
+ * typed pages that have been selected and are moved to the top with adaptive
+ * learning.
+ */
+
+function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+}
+AutoCompleteInput.prototype = {
+ constructor: AutoCompleteInput,
+
+ get minResultsForPopup() {
+ return 0;
+ },
+ get timeout() {
+ return 10;
+ },
+ get searchParam() {
+ return "";
+ },
+ get textValue() {
+ return "";
+ },
+ get disableAutoComplete() {
+ return false;
+ },
+ get completeDefaultIndex() {
+ return false;
+ },
+
+ get searchCount() {
+ return this.searches.length;
+ },
+ getSearchAt: function (aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function () {},
+ onSearchComplete: function() {},
+
+ get popupOpen() {
+ return false;
+ },
+ popup: {
+ set selectedIndex(aIndex) {},
+ invalidate: function () {},
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup])
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput])
+}
+
+/**
+ * Checks that autocomplete results are ordered correctly.
+ */
+function ensure_results(expected, searchTerm)
+{
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete.
+ let input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ input.onSearchComplete = function() {
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, expected.length);
+ for (let i = 0; i < controller.matchCount; i++) {
+ print("Testing for '" + expected[i].uri.spec + "' got '" + controller.getValueAt(i) + "'");
+ do_check_eq(controller.getValueAt(i), expected[i].uri.spec);
+ do_check_eq(controller.getStyleAt(i), expected[i].style);
+ }
+
+ deferEnsureResults.resolve();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+/**
+ * Asynchronous task that bumps up the rank for an uri.
+ */
+function* task_setCountRank(aURI, aCount, aRank, aSearch, aBookmark)
+{
+ // Bump up the visit count for the uri.
+ let visits = [];
+ for (let i = 0; i < aCount; i++) {
+ visits.push({ uri: aURI, visitDate: d1, transition: TRANSITION_TYPED });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+
+ // Make a nsIAutoCompleteController and friends for instrumentation feedback.
+ let thing = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ Ci.nsIAutoCompleteController]),
+ get popup() {
+ return thing;
+ },
+ get controller() {
+ return thing;
+ },
+ popupOpen: true,
+ selectedIndex: 0,
+ getValueAt: function() {
+ return aURI.spec;
+ },
+ searchString: aSearch
+ };
+
+ // Bump up the instrumentation feedback.
+ for (let i = 0; i < aRank; i++) {
+ Services.obs.notifyObservers(thing, "autocomplete-will-enter-text", null);
+ }
+
+ // If this is supposed to be a bookmark, add it.
+ if (aBookmark) {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test_book");
+
+ // And add the tag if we need to.
+ if (aBookmark == "tag") {
+ PlacesUtils.tagging.tagURI(aURI, ["test_tag"]);
+ }
+ }
+}
+
+/**
+ * Decay the adaptive entries by sending the daily idle topic.
+ */
+function doAdaptiveDecay()
+{
+ PlacesUtils.history.runInBatchMode({
+ runBatched: function() {
+ for (let i = 0; i < 10; i++) {
+ PlacesUtils.history.QueryInterface(Ci.nsIObserver)
+ .observe(null, "idle-daily", null);
+ }
+ }
+ }, this);
+}
+
+var uri1 = uri("http://site.tld/1");
+var uri2 = uri("http://site.tld/2");
+
+// d1 is some date for the page visit
+var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000;
+// c1 is larger (should show up higher) than c2
+var c1 = 10;
+var c2 = 1;
+// s1 is a partial match of s2
+var s0 = "";
+var s1 = "si";
+var s2 = "site";
+
+var observer = {
+ results: null,
+ search: null,
+ runCount: -1,
+ observe: function(aSubject, aTopic, aData)
+ {
+ if (--this.runCount > 0)
+ return;
+ ensure_results(this.results, this.search);
+ }
+};
+Services.obs.addObserver(observer, PlacesUtils.TOPIC_FEEDBACK_UPDATED, false);
+
+/**
+ * Make the result object for a given URI that will be passed to ensure_results.
+ */
+function makeResult(aURI, aStyle = "favicon") {
+ return {
+ uri: aURI,
+ style: aStyle,
+ };
+}
+
+var tests = [
+ // Test things without a search term.
+ function*() {
+ print("Test 0 same count, diff rank, same term; no search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+ function*() {
+ print("Test 1 same count, diff rank, same term; no search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c2, s2);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+ function*() {
+ print("Test 2 diff count, same rank, same term; no search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c2, c1, s2);
+ },
+ function*() {
+ print("Test 3 diff count, same rank, same term; no search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c2, c1, s2);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+
+ // Test things with a search term (exact match one, partial other).
+ function*() {
+ print("Test 4 same count, same rank, diff term; one exact/one partial search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s1);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+ function*() {
+ print("Test 5 same count, same rank, diff term; one exact/one partial search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c1, c1, s1);
+ },
+
+ // Test things with a search term (exact match both).
+ function*() {
+ print("Test 6 same count, diff rank, same term; both exact search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s1);
+ yield task_setCountRank(uri2, c1, c2, s1);
+ },
+ function*() {
+ print("Test 7 same count, diff rank, same term; both exact search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c2, s1);
+ yield task_setCountRank(uri2, c1, c1, s1);
+ },
+
+ // Test things with a search term (partial match both).
+ function*() {
+ print("Test 8 same count, diff rank, same term; both partial search");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2);
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+ function*() {
+ print("Test 9 same count, diff rank, same term; both partial search");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c2, s2);
+ yield task_setCountRank(uri2, c1, c1, s2);
+ },
+ function*() {
+ print("Test 10 same count, same rank, same term, decay first; exact match");
+ observer.results = [
+ makeResult(uri2),
+ makeResult(uri1),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri1, c1, c1, s1);
+ doAdaptiveDecay();
+ yield task_setCountRank(uri2, c1, c1, s1);
+ },
+ function*() {
+ print("Test 11 same count, same rank, same term, decay second; exact match");
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s1;
+ observer.runCount = c1 + c1;
+ yield task_setCountRank(uri2, c1, c1, s1);
+ doAdaptiveDecay();
+ yield task_setCountRank(uri1, c1, c1, s1);
+ },
+ // Test that bookmarks are hidden if the preferences are set right.
+ function*() {
+ print("Test 12 same count, diff rank, same term; no search; history only");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ observer.results = [
+ makeResult(uri1),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2, "bookmark");
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+ // Test that tags are shown if the preferences are set right.
+ function*() {
+ print("Test 13 same count, diff rank, same term; no search; history only with tag");
+ Services.prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.bookmark", false);
+ Services.prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ observer.results = [
+ makeResult(uri1, "tag"),
+ makeResult(uri2),
+ ];
+ observer.search = s0;
+ observer.runCount = c1 + c2;
+ yield task_setCountRank(uri1, c1, c1, s2, "tag");
+ yield task_setCountRank(uri2, c1, c2, s2);
+ },
+];
+
+/**
+ * This deferred object contains a promise that is resolved when the
+ * ensure_results function has finished its execution.
+ */
+var deferEnsureResults;
+
+/**
+ * Test adaptive autocomplete.
+ */
+add_task(function* test_adaptive()
+{
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+ for (let test of tests) {
+ // Cleanup.
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.tagsFolderId);
+ observer.runCount = -1;
+
+ let types = ["history", "bookmark", "openpage"];
+ for (let type of types) {
+ Services.prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+
+ yield PlacesTestUtils.clearHistory();
+
+ deferEnsureResults = Promise.defer();
+ yield test();
+ yield deferEnsureResults.promise;
+ }
+
+ Services.obs.removeObserver(observer, PlacesUtils.TOPIC_FEEDBACK_UPDATED);
+});
diff --git a/toolkit/components/places/tests/unit/test_adaptive_bug527311.js b/toolkit/components/places/tests/unit/test_adaptive_bug527311.js
new file mode 100644
index 000000000..024553bba
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_adaptive_bug527311.js
@@ -0,0 +1,141 @@
+/* -*- 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 = "http://adapt.mozilla.org/";
+const SEARCH_STRING = "adapt";
+const SUGGEST_TYPES = ["history", "bookmark", "openpage"];
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+var os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+var ps = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+const PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC =
+ "places-autocomplete-feedback-updated";
+
+function cleanup() {
+ for (let type of SUGGEST_TYPES) {
+ ps.clearUserPref("browser.urlbar.suggest." + type);
+ }
+}
+
+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: function ACI_getSearchAt(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchComplete: function ACI_onSearchComplete() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function() {},
+ invalidate: function() {},
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ onSearchBegin: function() {},
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+
+function check_results() {
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ let input = new AutoCompleteInput(["unifiedcomplete"]);
+ controller.input = input;
+
+ input.onSearchComplete = function() {
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+ do_check_eq(controller.matchCount, 0);
+
+ PlacesUtils.bookmarks.eraseEverything().then(() => {
+ cleanup();
+ do_test_finished();
+ });
+ };
+
+ controller.startSearch(SEARCH_STRING);
+}
+
+
+function addAdaptiveFeedback(aUrl, aSearch, aCallback) {
+ let observer = {
+ observe: function(aSubject, aTopic, aData) {
+ os.removeObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC);
+ do_timeout(0, aCallback);
+ }
+ };
+ os.addObserver(observer, PLACES_AUTOCOMPLETE_FEEDBACK_UPDATED_TOPIC, false);
+
+ let thing = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ Ci.nsIAutoCompleteController]),
+ get popup() { return thing; },
+ get controller() { return thing; },
+ popupOpen: true,
+ selectedIndex: 0,
+ getValueAt: () => aUrl,
+ searchString: aSearch
+ };
+
+ os.notifyObservers(thing, "autocomplete-will-enter-text", null);
+}
+
+
+function run_test() {
+ do_test_pending();
+
+ // Add a bookmark to our url.
+ bs.insertBookmark(bs.unfiledBookmarksFolder, uri(TEST_URL),
+ bs.DEFAULT_INDEX, "test_book");
+ // We want to search only history.
+ for (let type of SUGGEST_TYPES) {
+ type == "history" ? ps.setBoolPref("browser.urlbar.suggest." + type, true)
+ : ps.setBoolPref("browser.urlbar.suggest." + type, false);
+ }
+
+ // Add an adaptive entry.
+ addAdaptiveFeedback(TEST_URL, SEARCH_STRING, check_results);
+}
diff --git a/toolkit/components/places/tests/unit/test_analyze.js b/toolkit/components/places/tests/unit/test_analyze.js
new file mode 100644
index 000000000..456270101
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_analyze.js
@@ -0,0 +1,28 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests sqlite_sta1 table exists, it should be created by analyze.
+// Since the bookmark roots are created when the DB is created (bug 704855),
+// the table will contain data.
+
+function run_test() {
+ do_test_pending();
+
+ let stmt = DBConn().createAsyncStatement(
+ "SELECT ROWID FROM sqlite_stat1"
+ );
+ stmt.executeAsync({
+ _gotResult: false,
+ handleResult: function(aResultSet) {
+ this._gotResult = true;
+ },
+ handleError: function(aError) {
+ do_throw("Unexpected error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function(aReason) {
+ do_check_true(this._gotResult);
+ do_test_finished();
+ }
+ });
+ stmt.finalize();
+}
diff --git a/toolkit/components/places/tests/unit/test_annotations.js b/toolkit/components/places/tests/unit/test_annotations.js
new file mode 100644
index 000000000..a37d7e6c9
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_annotations.js
@@ -0,0 +1,363 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Get bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].getService(Ci.nsINavBookmarksService);
+} catch (ex) {
+ do_throw("Could not get nav-bookmarks-service\n");
+}
+
+// Get annotation service
+try {
+ var annosvc= Cc["@mozilla.org/browser/annotation-service;1"].getService(Ci.nsIAnnotationService);
+} catch (ex) {
+ do_throw("Could not get annotation service\n");
+}
+
+var annoObserver = {
+ PAGE_lastSet_URI: "",
+ PAGE_lastSet_AnnoName: "",
+
+ onPageAnnotationSet: function(aURI, aName) {
+ this.PAGE_lastSet_URI = aURI.spec;
+ this.PAGE_lastSet_AnnoName = aName;
+ },
+
+ ITEM_lastSet_Id: -1,
+ ITEM_lastSet_AnnoName: "",
+ onItemAnnotationSet: function(aItemId, aName) {
+ this.ITEM_lastSet_Id = aItemId;
+ this.ITEM_lastSet_AnnoName = aName;
+ },
+
+ PAGE_lastRemoved_URI: "",
+ PAGE_lastRemoved_AnnoName: "",
+ onPageAnnotationRemoved: function(aURI, aName) {
+ this.PAGE_lastRemoved_URI = aURI.spec;
+ this.PAGE_lastRemoved_AnnoName = aName;
+ },
+
+ ITEM_lastRemoved_Id: -1,
+ ITEM_lastRemoved_AnnoName: "",
+ onItemAnnotationRemoved: function(aItemId, aName) {
+ this.ITEM_lastRemoved_Id = aItemId;
+ this.ITEM_lastRemoved_AnnoName = aName;
+ }
+};
+
+// main
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ var testURI = uri("http://mozilla.com/");
+ var testItemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, "");
+ var testAnnoName = "moz-test-places/annotations";
+ var testAnnoVal = "test";
+
+ annosvc.addObserver(annoObserver);
+ // create new string annotation
+ try {
+ annosvc.setPageAnnotation(testURI, testAnnoName, testAnnoVal, 0, 0);
+ } catch (ex) {
+ do_throw("unable to add page-annotation");
+ }
+ do_check_eq(annoObserver.PAGE_lastSet_URI, testURI.spec);
+ do_check_eq(annoObserver.PAGE_lastSet_AnnoName, testAnnoName);
+
+ // get string annotation
+ do_check_true(annosvc.pageHasAnnotation(testURI, testAnnoName));
+ var storedAnnoVal = annosvc.getPageAnnotation(testURI, testAnnoName);
+ do_check_true(testAnnoVal === storedAnnoVal);
+ // string item-annotation
+ try {
+ var lastModified = bmsvc.getItemLastModified(testItemId);
+ // Verify that lastModified equals dateAdded before we set the annotation.
+ do_check_eq(lastModified, bmsvc.getItemDateAdded(testItemId));
+ // Workaround possible VM timers issues moving last modified to the past.
+ bmsvc.setItemLastModified(testItemId, --lastModified);
+ annosvc.setItemAnnotation(testItemId, testAnnoName, testAnnoVal, 0, 0);
+ var lastModified2 = bmsvc.getItemLastModified(testItemId);
+ // verify that setting the annotation updates the last modified time
+ do_check_true(lastModified2 > lastModified);
+ } catch (ex) {
+ do_throw("unable to add item annotation");
+ }
+ do_check_eq(annoObserver.ITEM_lastSet_Id, testItemId);
+ do_check_eq(annoObserver.ITEM_lastSet_AnnoName, testAnnoName);
+
+ try {
+ var annoVal = annosvc.getItemAnnotation(testItemId, testAnnoName);
+ // verify the anno value
+ do_check_true(testAnnoVal === annoVal);
+ } catch (ex) {
+ do_throw("unable to get item annotation");
+ }
+
+ // test getPagesWithAnnotation
+ var uri2 = uri("http://www.tests.tld");
+ yield PlacesTestUtils.addVisits(uri2);
+ annosvc.setPageAnnotation(uri2, testAnnoName, testAnnoVal, 0, 0);
+ var pages = annosvc.getPagesWithAnnotation(testAnnoName);
+ do_check_eq(pages.length, 2);
+ // Don't rely on the order
+ do_check_false(pages[0].equals(pages[1]));
+ do_check_true(pages[0].equals(testURI) || pages[1].equals(testURI));
+ do_check_true(pages[0].equals(uri2) || pages[1].equals(uri2));
+
+ // test getItemsWithAnnotation
+ var testItemId2 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri2, -1, "");
+ annosvc.setItemAnnotation(testItemId2, testAnnoName, testAnnoVal, 0, 0);
+ var items = annosvc.getItemsWithAnnotation(testAnnoName);
+ do_check_eq(items.length, 2);
+ // Don't rely on the order
+ do_check_true(items[0] != items[1]);
+ do_check_true(items[0] == testItemId || items[1] == testItemId);
+ do_check_true(items[0] == testItemId2 || items[1] == testItemId2);
+
+ // get annotation that doesn't exist
+ try {
+ annosvc.getPageAnnotation(testURI, "blah");
+ do_throw("fetching page-annotation that doesn't exist, should've thrown");
+ } catch (ex) {}
+ try {
+ annosvc.getItemAnnotation(testURI, "blah");
+ do_throw("fetching item-annotation that doesn't exist, should've thrown");
+ } catch (ex) {}
+
+ // get annotation info
+ var flags = {}, exp = {}, storageType = {};
+ annosvc.getPageAnnotationInfo(testURI, testAnnoName, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_STRING);
+ annosvc.getItemAnnotationInfo(testItemId, testAnnoName, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_STRING);
+
+ // get annotation names for a uri
+ var annoNames = annosvc.getPageAnnotationNames(testURI);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "moz-test-places/annotations");
+
+ // get annotation names for an item
+ annoNames = annosvc.getItemAnnotationNames(testItemId);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "moz-test-places/annotations");
+
+ // copy annotations to another uri
+ var newURI = uri("http://mozilla.org");
+ yield PlacesTestUtils.addVisits(newURI);
+ annosvc.setPageAnnotation(testURI, "oldAnno", "new", 0, 0);
+ annosvc.setPageAnnotation(newURI, "oldAnno", "old", 0, 0);
+ annoNames = annosvc.getPageAnnotationNames(newURI);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "oldAnno");
+ var oldAnnoNames = annosvc.getPageAnnotationNames(testURI);
+ do_check_eq(oldAnnoNames.length, 2);
+ var copiedAnno = oldAnnoNames[0];
+ annosvc.copyPageAnnotations(testURI, newURI, false);
+ var newAnnoNames = annosvc.getPageAnnotationNames(newURI);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.pageHasAnnotation(newURI, "oldAnno"));
+ do_check_true(annosvc.pageHasAnnotation(newURI, copiedAnno));
+ do_check_eq(annosvc.getPageAnnotation(newURI, "oldAnno"), "old");
+ annosvc.setPageAnnotation(newURI, "oldAnno", "new", 0, 0);
+ annosvc.copyPageAnnotations(testURI, newURI, true);
+ newAnnoNames = annosvc.getPageAnnotationNames(newURI);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.pageHasAnnotation(newURI, "oldAnno"));
+ do_check_true(annosvc.pageHasAnnotation(newURI, copiedAnno));
+ do_check_eq(annosvc.getPageAnnotation(newURI, "oldAnno"), "new");
+
+
+ // copy annotations to another item
+ newURI = uri("http://mozilla.org");
+ var newItemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, newURI, -1, "");
+ var itemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, "");
+ annosvc.setItemAnnotation(itemId, "oldAnno", "new", 0, 0);
+ annosvc.setItemAnnotation(itemId, "testAnno", "test", 0, 0);
+ annosvc.setItemAnnotation(newItemId, "oldAnno", "old", 0, 0);
+ annoNames = annosvc.getItemAnnotationNames(newItemId);
+ do_check_eq(annoNames.length, 1);
+ do_check_eq(annoNames[0], "oldAnno");
+ oldAnnoNames = annosvc.getItemAnnotationNames(itemId);
+ do_check_eq(oldAnnoNames.length, 2);
+ copiedAnno = oldAnnoNames[0];
+ annosvc.copyItemAnnotations(itemId, newItemId, false);
+ newAnnoNames = annosvc.getItemAnnotationNames(newItemId);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.itemHasAnnotation(newItemId, "oldAnno"));
+ do_check_true(annosvc.itemHasAnnotation(newItemId, copiedAnno));
+ do_check_eq(annosvc.getItemAnnotation(newItemId, "oldAnno"), "old");
+ annosvc.setItemAnnotation(newItemId, "oldAnno", "new", 0, 0);
+ annosvc.copyItemAnnotations(itemId, newItemId, true);
+ newAnnoNames = annosvc.getItemAnnotationNames(newItemId);
+ do_check_eq(newAnnoNames.length, 2);
+ do_check_true(annosvc.itemHasAnnotation(newItemId, "oldAnno"));
+ do_check_true(annosvc.itemHasAnnotation(newItemId, copiedAnno));
+ do_check_eq(annosvc.getItemAnnotation(newItemId, "oldAnno"), "new");
+
+ // test int32 anno type
+ var int32Key = testAnnoName + "/types/Int32";
+ var int32Val = 23;
+ annosvc.setPageAnnotation(testURI, int32Key, int32Val, 0, 0);
+ do_check_true(annosvc.pageHasAnnotation(testURI, int32Key));
+ flags = {}, exp = {}, storageType = {};
+ annosvc.getPageAnnotationInfo(testURI, int32Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_INT32);
+ var storedVal = annosvc.getPageAnnotation(testURI, int32Key);
+ do_check_true(int32Val === storedVal);
+ annosvc.setItemAnnotation(testItemId, int32Key, int32Val, 0, 0);
+ do_check_true(annosvc.itemHasAnnotation(testItemId, int32Key));
+ annosvc.getItemAnnotationInfo(testItemId, int32Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getItemAnnotation(testItemId, int32Key);
+ do_check_true(int32Val === storedVal);
+
+ // test int64 anno type
+ var int64Key = testAnnoName + "/types/Int64";
+ var int64Val = 4294967296;
+ annosvc.setPageAnnotation(testURI, int64Key, int64Val, 0, 0);
+ annosvc.getPageAnnotationInfo(testURI, int64Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getPageAnnotation(testURI, int64Key);
+ do_check_true(int64Val === storedVal);
+ annosvc.setItemAnnotation(testItemId, int64Key, int64Val, 0, 0);
+ do_check_true(annosvc.itemHasAnnotation(testItemId, int64Key));
+ annosvc.getItemAnnotationInfo(testItemId, int64Key, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getItemAnnotation(testItemId, int64Key);
+ do_check_true(int64Val === storedVal);
+
+ // test double anno type
+ var doubleKey = testAnnoName + "/types/Double";
+ var doubleVal = 0.000002342;
+ annosvc.setPageAnnotation(testURI, doubleKey, doubleVal, 0, 0);
+ annosvc.getPageAnnotationInfo(testURI, doubleKey, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ storedVal = annosvc.getPageAnnotation(testURI, doubleKey);
+ do_check_true(doubleVal === storedVal);
+ annosvc.setItemAnnotation(testItemId, doubleKey, doubleVal, 0, 0);
+ do_check_true(annosvc.itemHasAnnotation(testItemId, doubleKey));
+ annosvc.getItemAnnotationInfo(testItemId, doubleKey, flags, exp, storageType);
+ do_check_eq(flags.value, 0);
+ do_check_eq(exp.value, 0);
+ do_check_eq(storageType.value, Ci.nsIAnnotationService.TYPE_DOUBLE);
+ storedVal = annosvc.getItemAnnotation(testItemId, doubleKey);
+ do_check_true(doubleVal === storedVal);
+
+ // test annotation removal
+ annosvc.removePageAnnotation(testURI, int32Key);
+
+ annosvc.setItemAnnotation(testItemId, testAnnoName, testAnnoVal, 0, 0);
+ // verify that removing an annotation updates the last modified date
+ var lastModified3 = bmsvc.getItemLastModified(testItemId);
+ // Workaround possible VM timers issues moving last modified to the past.
+ bmsvc.setItemLastModified(testItemId, --lastModified3);
+ annosvc.removeItemAnnotation(testItemId, int32Key);
+ var lastModified4 = bmsvc.getItemLastModified(testItemId);
+ do_print("verify that removing an annotation updates the last modified date");
+ do_print("lastModified3 = " + lastModified3);
+ do_print("lastModified4 = " + lastModified4);
+ do_check_true(lastModified4 > lastModified3);
+
+ do_check_eq(annoObserver.PAGE_lastRemoved_URI, testURI.spec);
+ do_check_eq(annoObserver.PAGE_lastRemoved_AnnoName, int32Key);
+ do_check_eq(annoObserver.ITEM_lastRemoved_Id, testItemId);
+ do_check_eq(annoObserver.ITEM_lastRemoved_AnnoName, int32Key);
+
+ // test that getItems/PagesWithAnnotation returns an empty array after
+ // removing all items/pages which had the annotation set, see bug 380317.
+ do_check_eq(annosvc.getItemsWithAnnotation(int32Key).length, 0);
+ do_check_eq(annosvc.getPagesWithAnnotation(int32Key).length, 0);
+
+ // Setting item annotations on invalid item ids should throw
+ var invalidIds = [-1, 0, 37643];
+ for (var id of invalidIds) {
+ try {
+ annosvc.setItemAnnotation(id, "foo", "bar", 0, 0);
+ do_throw("setItemAnnotation* should throw for invalid item id: " + id)
+ }
+ catch (ex) { }
+ }
+
+ // setting an annotation with EXPIRE_HISTORY for an item should throw
+ itemId = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, -1, "");
+ try {
+ annosvc.setItemAnnotation(itemId, "foo", "bar", 0, annosvc.EXPIRE_WITH_HISTORY);
+ do_throw("setting an item annotation with EXPIRE_HISTORY should throw");
+ }
+ catch (ex) {
+ }
+
+ annosvc.removeObserver(annoObserver);
+});
+
+add_test(function test_getAnnotationsHavingName() {
+ let uri = NetUtil.newURI("http://cat.mozilla.org");
+ let id = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "cat");
+ let fid = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.unfiledBookmarksFolderId, "pillow",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ const ANNOS = {
+ "int": 7,
+ "double": 7.7,
+ "string": "seven"
+ };
+ for (let name in ANNOS) {
+ PlacesUtils.annotations.setPageAnnotation(
+ uri, name, ANNOS[name], 0,
+ PlacesUtils.annotations.EXPIRE_SESSION);
+ PlacesUtils.annotations.setItemAnnotation(
+ id, name, ANNOS[name], 0,
+ PlacesUtils.annotations.EXPIRE_SESSION);
+ PlacesUtils.annotations.setItemAnnotation(
+ fid, name, ANNOS[name], 0,
+ PlacesUtils.annotations.EXPIRE_SESSION);
+ }
+
+ for (let name in ANNOS) {
+ let results = PlacesUtils.annotations.getAnnotationsWithName(name);
+ do_check_eq(results.length, 3);
+
+ for (let result of results) {
+ do_check_eq(result.annotationName, name);
+ do_check_eq(result.annotationValue, ANNOS[name]);
+ if (result.uri)
+ do_check_true(result.uri.equals(uri));
+ else
+ do_check_true(result.itemId > 0);
+
+ if (result.itemId != -1) {
+ if (result.uri)
+ do_check_eq(result.itemId, id);
+ else
+ do_check_eq(result.itemId, fid);
+ do_check_guid_for_bookmark(result.itemId, result.guid);
+ }
+ else {
+ do_check_guid_for_uri(result.uri, result.guid);
+ }
+ }
+ }
+
+ run_next_test();
+});
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 000000000..7296fe061
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_asyncExecuteLegacyQueries.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This is a test for asyncExecuteLegacyQueries API.
+
+var tests = [
+
+function test_history_query() {
+ let uri = NetUtil.newURI("http://test.visit.mozilla.com/");
+ let title = "Test visit";
+ PlacesTestUtils.addVisits({ uri: uri, title: title }).then(function () {
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING;
+ let query = PlacesUtils.history.getNewQuery();
+
+ PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .asyncExecuteLegacyQueries([query], 1, options, {
+ handleResult: function (aResultSet) {
+ for (let row; (row = aResultSet.getNextRow());) {
+ try {
+ do_check_eq(row.getResultByIndex(1), uri.spec);
+ do_check_eq(row.getResultByIndex(2), title);
+ } catch (e) {
+ do_throw("Error while fetching page data.");
+ }
+ }
+ },
+ handleError: function (aError) {
+ do_throw("Async execution error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function (aReason) {
+ run_next_test();
+ },
+ });
+ });
+},
+
+function test_bookmarks_query() {
+ let uri = NetUtil.newURI("http://test.bookmark.mozilla.com/");
+ let title = "Test bookmark";
+ bookmark(uri, title);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.sortingMode = Ci.nsINavHistoryQueryOptions.SORT_BY_LASMODIFIED_DESCENDING;
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let query = PlacesUtils.history.getNewQuery();
+
+ PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
+ .asyncExecuteLegacyQueries([query], 1, options, {
+ handleResult: function (aResultSet) {
+ for (let row; (row = aResultSet.getNextRow());) {
+ try {
+ do_check_eq(row.getResultByIndex(1), uri.spec);
+ do_check_eq(row.getResultByIndex(2), title);
+ } catch (e) {
+ do_throw("Error while fetching page data.");
+ }
+ }
+ },
+ handleError: function (aError) {
+ do_throw("Async execution error (" + aError.result + "): " + aError.message);
+ },
+ handleCompletion: function (aReason) {
+ run_next_test();
+ },
+ });
+},
+
+];
+
+function bookmark(aURI, aTitle)
+{
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ aTitle);
+}
+
+function run_test()
+{
+ do_test_pending();
+ run_next_test();
+}
+
+function run_next_test() {
+ if (tests.length == 0) {
+ do_test_finished();
+ return;
+ }
+
+ Promise.all([
+ PlacesTestUtils.clearHistory(),
+ PlacesUtils.bookmarks.eraseEverything()
+ ]).then(tests.shift());
+}
diff --git a/toolkit/components/places/tests/unit/test_async_history_api.js b/toolkit/components/places/tests/unit/test_async_history_api.js
new file mode 100644
index 000000000..a012fcda2
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_history_api.js
@@ -0,0 +1,1118 @@
+/**
+ * This file tests the async history API exposed by mozIAsyncHistory.
+ */
+
+// Globals
+
+const TEST_DOMAIN = "http://mozilla.org/";
+const URI_VISIT_SAVED = "uri-visit-saved";
+const RECENT_EVENT_THRESHOLD = 15 * 60 * 1000000;
+
+// Helpers
+/**
+ * Object that represents a mozIVisitInfo object.
+ *
+ * @param [optional] aTransitionType
+ * The transition type of the visit. Defaults to TRANSITION_LINK if not
+ * provided.
+ * @param [optional] aVisitTime
+ * The time of the visit. Defaults to now if not provided.
+ */
+function VisitInfo(aTransitionType,
+ aVisitTime) {
+ this.transitionType =
+ aTransitionType === undefined ? TRANSITION_LINK : aTransitionType;
+ this.visitDate = aVisitTime || Date.now() * 1000;
+}
+
+function promiseUpdatePlaces(aPlaces) {
+ return new Promise((resolve, reject) => {
+ PlacesUtils.asyncHistory.updatePlaces(aPlaces, {
+ _errors: [],
+ _results: [],
+ handleError(aResultCode, aPlace) {
+ this._errors.push({ resultCode: aResultCode, info: aPlace});
+ },
+ handleResult(aPlace) {
+ this._results.push(aPlace);
+ },
+ handleCompletion() {
+ resolve({ errors: this._errors, results: this._results });
+ }
+ });
+ });
+}
+
+/**
+ * Listens for a title change notification, and calls aCallback when it gets it.
+ *
+ * @param aURI
+ * The URI of the page we expect a notification for.
+ * @param aExpectedTitle
+ * The expected title of the URI we expect a notification for.
+ * @param aCallback
+ * The method to call when we have gotten the proper notification about
+ * the title changing.
+ */
+function TitleChangedObserver(aURI,
+ aExpectedTitle,
+ aCallback) {
+ this.uri = aURI;
+ this.expectedTitle = aExpectedTitle;
+ this.callback = aCallback;
+}
+TitleChangedObserver.prototype = {
+ __proto__: NavHistoryObserver.prototype,
+ onTitleChanged(aURI, aTitle, aGUID) {
+ do_print("onTitleChanged(" + aURI.spec + ", " + aTitle + ", " + aGUID + ")");
+ if (!this.uri.equals(aURI)) {
+ return;
+ }
+ do_check_eq(aTitle, this.expectedTitle);
+ do_check_guid_for_uri(aURI, aGUID);
+ this.callback();
+ },
+};
+
+/**
+ * Listens for a visit notification, and calls aCallback when it gets it.
+ *
+ * @param aURI
+ * The URI of the page we expect a notification for.
+ * @param aCallback
+ * The method to call when we have gotten the proper notification about
+ * being visited.
+ */
+function VisitObserver(aURI,
+ aGUID,
+ aCallback)
+{
+ this.uri = aURI;
+ this.guid = aGUID;
+ this.callback = aCallback;
+}
+VisitObserver.prototype = {
+ __proto__: NavHistoryObserver.prototype,
+ onVisit: function(aURI,
+ aVisitId,
+ aTime,
+ aSessionId,
+ aReferringId,
+ aTransitionType,
+ aGUID)
+ {
+ do_print("onVisit(" + aURI.spec + ", " + aVisitId + ", " + aTime +
+ ", " + aSessionId + ", " + aReferringId + ", " +
+ aTransitionType + ", " + aGUID + ")");
+ if (!this.uri.equals(aURI) || this.guid != aGUID) {
+ return;
+ }
+ this.callback(aTime, aTransitionType);
+ },
+};
+
+/**
+ * Tests that a title was set properly in the database.
+ *
+ * @param aURI
+ * The uri to check.
+ * @param aTitle
+ * The expected title in the database.
+ */
+function do_check_title_for_uri(aURI,
+ aTitle)
+{
+ let stack = Components.stack.caller;
+ let stmt = DBConn().createStatement(
+ `SELECT title
+ FROM moz_places
+ WHERE url_hash = hash(:url) AND url = :url`
+ );
+ stmt.params.url = aURI.spec;
+ do_check_true(stmt.executeStep(), stack);
+ do_check_eq(stmt.row.title, aTitle, stack);
+ stmt.finalize();
+}
+
+// Test Functions
+
+add_task(function* test_interface_exists() {
+ let history = Cc["@mozilla.org/browser/history;1"].getService(Ci.nsISupports);
+ do_check_true(history instanceof Ci.mozIAsyncHistory);
+});
+
+add_task(function* test_invalid_uri_throws() {
+ // First, test passing in nothing.
+ let place = {
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Now, test other bogus things.
+ const TEST_VALUES = [
+ null,
+ undefined,
+ {},
+ [],
+ TEST_DOMAIN + "test_invalid_id_throws",
+ ];
+ for (let i = 0; i < TEST_VALUES.length; i++) {
+ place.uri = TEST_VALUES[i];
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+});
+
+add_task(function* test_invalid_places_throws() {
+ // First, test passing in nothing.
+ try {
+ PlacesUtils.asyncHistory.updatePlaces();
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS);
+ }
+
+ // Now, test other bogus things.
+ const TEST_VALUES = [
+ null,
+ undefined,
+ {},
+ [],
+ "",
+ ];
+ for (let i = 0; i < TEST_VALUES.length; i++) {
+ let value = TEST_VALUES[i];
+ try {
+ yield promiseUpdatePlaces(value);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+});
+
+add_task(function* test_invalid_guid_throws() {
+ // First check invalid length guid.
+ let place = {
+ guid: "BAD_GUID",
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_invalid_guid_throws"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Now check invalid character guid.
+ place.guid = "__BADGUID+__";
+ do_check_eq(place.guid.length, 12);
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_no_visits_throws() {
+ const TEST_URI =
+ NetUtil.newURI(TEST_DOMAIN + "test_no_id_or_guid_no_visits_throws");
+ const TEST_GUID = "_RANDOMGUID_";
+
+ let log_test_conditions = function(aPlace) {
+ let str = "Testing place with " +
+ (aPlace.uri ? "uri" : "no uri") + ", " +
+ (aPlace.guid ? "guid" : "no guid") + ", " +
+ (aPlace.visits ? "visits array" : "no visits array");
+ do_print(str);
+ };
+
+ // Loop through every possible case. Note that we don't actually care about
+ // the case where we have no uri, place id, or guid (covered by another test),
+ // but it is easier to just make sure it too throws than to exclude it.
+ let place = { };
+ for (let uri = 1; uri >= 0; uri--) {
+ place.uri = uri ? TEST_URI : undefined;
+
+ for (let guid = 1; guid >= 0; guid--) {
+ place.guid = guid ? TEST_GUID : undefined;
+
+ for (let visits = 1; visits >= 0; visits--) {
+ place.visits = visits ? [] : undefined;
+
+ log_test_conditions(place);
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+ }
+ }
+ }
+});
+
+add_task(function* test_add_visit_no_date_throws() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_date_throws"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ delete place.visits[0].visitDate;
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_add_visit_no_transitionType_throws() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit_no_transitionType_throws"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ delete place.visits[0].transitionType;
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_add_visit_invalid_transitionType_throws() {
+ // First, test something that has a transition type lower than the first one.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_add_visit_invalid_transitionType_throws"),
+ visits: [
+ new VisitInfo(TRANSITION_LINK - 1),
+ ],
+ };
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+
+ // Now, test something that has a transition type greater than the last one.
+ place.visits[0] = new VisitInfo(TRANSITION_RELOAD + 1);
+ try {
+ yield promiseUpdatePlaces(place);
+ do_throw("Should have thrown!");
+ }
+ catch (e) {
+ do_check_eq(e.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_non_addable_uri_errors() {
+ // Array of protocols that nsINavHistoryService::canAddURI returns false for.
+ const URLS = [
+ "about:config",
+ "imap://cyrus.andrew.cmu.edu/archive.imap",
+ "news://new.mozilla.org/mozilla.dev.apps.firefox",
+ "mailbox:Inbox",
+ "moz-anno:favicon:http://mozilla.org/made-up-favicon",
+ "view-source:http://mozilla.org",
+ "chrome://browser/content/browser.xul",
+ "resource://gre-resources/hiddenWindow.html",
+ "data:,Hello%2C%20World!",
+ "wyciwyg:/0/http://mozilla.org",
+ "javascript:alert('hello wolrd!');",
+ "blob:foo",
+ ];
+ let places = [];
+ URLS.forEach(function(url) {
+ try {
+ let place = {
+ uri: NetUtil.newURI(url),
+ title: "test for " + url,
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ places.push(place);
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ // NetUtil.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.
+ do_print("Could not construct URI for '" + url + "'; ignoring");
+ }
+ });
+
+ let placesResult = yield promiseUpdatePlaces(places);
+ if (placesResult.results.length > 0) {
+ do_throw("Unexpected success.");
+ }
+ for (let place of placesResult.errors) {
+ do_print("Checking '" + place.info.uri.spec + "'");
+ do_check_eq(place.resultCode, Cr.NS_ERROR_INVALID_ARG);
+ do_check_false(yield promiseIsURIVisited(place.info.uri));
+ }
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_duplicate_guid_errors() {
+ // This test ensures that trying to add a visit, with a guid already found in
+ // another visit, fails.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_first"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(placeInfo.uri));
+
+ let badPlace = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_duplicate_guid_fails_second"),
+ visits: [
+ new VisitInfo(),
+ ],
+ guid: placeInfo.guid,
+ };
+
+ do_check_false(yield promiseIsURIVisited(badPlace.uri));
+ placesResult = yield promiseUpdatePlaces(badPlace);
+ if (placesResult.results.length > 0) {
+ do_throw("Unexpected success.");
+ }
+ let badPlaceInfo = placesResult.errors[0];
+ do_check_eq(badPlaceInfo.resultCode, Cr.NS_ERROR_STORAGE_CONSTRAINT);
+ do_check_false(yield promiseIsURIVisited(badPlaceInfo.info.uri));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_invalid_referrerURI_ignored() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_invalid_referrerURI_ignored"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ place.visits[0].referrerURI = NetUtil.newURI(place.uri.spec + "_unvisistedURI");
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(placeInfo.uri));
+
+ // Check to make sure we do not visit the invalid referrer.
+ do_check_false(yield promiseIsURIVisited(place.visits[0].referrerURI));
+
+ // Check to make sure from_visit is zero in database.
+ let stmt = DBConn().createStatement(
+ `SELECT from_visit
+ FROM moz_historyvisits
+ WHERE id = :visit_id`
+ );
+ stmt.params.visit_id = placeInfo.visits[0].visitId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.from_visit, 0);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_nonnsIURI_referrerURI_ignored() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_nonnsIURI_referrerURI_ignored"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ place.visits[0].referrerURI = place.uri.spec + "_nonnsIURI";
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(placeInfo.uri));
+
+ // Check to make sure from_visit is zero in database.
+ let stmt = DBConn().createStatement(
+ `SELECT from_visit
+ FROM moz_historyvisits
+ WHERE id = :visit_id`
+ );
+ stmt.params.visit_id = placeInfo.visits[0].visitId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.from_visit, 0);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_old_referrer_ignored() {
+ // This tests that a referrer for a visit which is not recent (specifically,
+ // older than 15 minutes as per RECENT_EVENT_THRESHOLD) is not saved by
+ // updatePlaces.
+ let oldTime = (Date.now() * 1000) - (RECENT_EVENT_THRESHOLD + 1);
+ let referrerPlace = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_referrer"),
+ visits: [
+ new VisitInfo(TRANSITION_LINK, oldTime),
+ ],
+ };
+
+ // First we must add our referrer to the history so that it is not ignored
+ // as being invalid.
+ do_check_false(yield promiseIsURIVisited(referrerPlace.uri));
+ let placesResult = yield promiseUpdatePlaces(referrerPlace);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+
+ // Now that the referrer is added, we can add a page with a valid
+ // referrer to determine if the recency of the referrer is taken into
+ // account.
+ do_check_true(yield promiseIsURIVisited(referrerPlace.uri));
+
+ let visitInfo = new VisitInfo();
+ visitInfo.referrerURI = referrerPlace.uri;
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_old_referrer_ignored_page"),
+ visits: [
+ visitInfo,
+ ],
+ };
+
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(place.uri));
+
+ // Though the visit will not contain the referrer, we must examine the
+ // database to be sure.
+ do_check_eq(placeInfo.visits[0].referrerURI, null);
+ let stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_historyvisits
+ JOIN moz_places h ON h.id = place_id
+ WHERE url_hash = hash(:page_url) AND url = :page_url
+ AND from_visit = 0`
+ );
+ stmt.params.page_url = place.uri.spec;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, 1);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_place_id_ignored() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_first"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ do_check_true(yield promiseIsURIVisited(place.uri));
+
+ let placeId = placeInfo.placeId;
+ do_check_neq(placeId, 0);
+
+ let badPlace = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_place_id_ignored_second"),
+ visits: [
+ new VisitInfo(),
+ ],
+ placeId: placeId,
+ };
+
+ do_check_false(yield promiseIsURIVisited(badPlace.uri));
+ placesResult = yield promiseUpdatePlaces(badPlace);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ placeInfo = placesResult.results[0];
+
+ do_check_neq(placeInfo.placeId, placeId);
+ do_check_true(yield promiseIsURIVisited(badPlace.uri));
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_handleCompletion_called_when_complete() {
+ // We test a normal visit, and embeded visit, and a uri that would fail
+ // the canAddURI test to make sure that the notification happens after *all*
+ // of them have had a callback.
+ let places = [
+ { uri: NetUtil.newURI(TEST_DOMAIN +
+ "test_handleCompletion_called_when_complete"),
+ visits: [
+ new VisitInfo(),
+ new VisitInfo(TRANSITION_EMBED),
+ ],
+ },
+ { uri: NetUtil.newURI("data:,Hello%2C%20World!"),
+ visits: [
+ new VisitInfo(),
+ ],
+ },
+ ];
+ do_check_false(yield promiseIsURIVisited(places[0].uri));
+ do_check_false(yield promiseIsURIVisited(places[1].uri));
+
+ const EXPECTED_COUNT_SUCCESS = 2;
+ const EXPECTED_COUNT_FAILURE = 1;
+
+ let {results, errors} = yield promiseUpdatePlaces(places);
+
+ do_check_eq(results.length, EXPECTED_COUNT_SUCCESS);
+ do_check_eq(errors.length, EXPECTED_COUNT_FAILURE);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_add_visit() {
+ const VISIT_TIME = Date.now() * 1000;
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_add_visit"),
+ title: "test_add_visit title",
+ visits: [],
+ };
+ for (let t in PlacesUtils.history.TRANSITIONS) {
+ let transitionType = PlacesUtils.history.TRANSITIONS[t];
+ place.visits.push(new VisitInfo(transitionType, VISIT_TIME));
+ }
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let callbackCount = 0;
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ for (let placeInfo of placesResult.results) {
+ do_check_true(yield promiseIsURIVisited(place.uri));
+
+ // Check mozIPlaceInfo properties.
+ do_check_true(place.uri.equals(placeInfo.uri));
+ do_check_eq(placeInfo.frecency, -1); // We don't pass frecency here!
+ do_check_eq(placeInfo.title, place.title);
+
+ // Check mozIVisitInfo properties.
+ let visits = placeInfo.visits;
+ do_check_eq(visits.length, 1);
+ let visit = visits[0];
+ do_check_eq(visit.visitDate, VISIT_TIME);
+ do_check_true(Object.values(PlacesUtils.history.TRANSITIONS).includes(visit.transitionType));
+ do_check_true(visit.referrerURI === null);
+
+ // For TRANSITION_EMBED visits, many properties will always be zero or
+ // undefined.
+ if (visit.transitionType == TRANSITION_EMBED) {
+ // Check mozIPlaceInfo properties.
+ do_check_eq(placeInfo.placeId, 0, '//');
+ do_check_eq(placeInfo.guid, null);
+
+ // Check mozIVisitInfo properties.
+ do_check_eq(visit.visitId, 0);
+ }
+ // But they should be valid for non-embed visits.
+ else {
+ // Check mozIPlaceInfo properties.
+ do_check_true(placeInfo.placeId > 0);
+ do_check_valid_places_guid(placeInfo.guid);
+
+ // Check mozIVisitInfo properties.
+ do_check_true(visit.visitId > 0);
+ }
+
+ // If we have had all of our callbacks, continue running tests.
+ if (++callbackCount == place.visits.length) {
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ }
+ }
+});
+
+add_task(function* test_properties_saved() {
+ // Check each transition type to make sure it is saved properly.
+ let places = [];
+ for (let t in PlacesUtils.history.TRANSITIONS) {
+ let transitionType = PlacesUtils.history.TRANSITIONS[t];
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_properties_saved/" +
+ transitionType),
+ title: "test_properties_saved test",
+ visits: [
+ new VisitInfo(transitionType),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+ places.push(place);
+ }
+
+ let callbackCount = 0;
+ let placesResult = yield promiseUpdatePlaces(places);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ for (let placeInfo of placesResult.results) {
+ let uri = placeInfo.uri;
+ do_check_true(yield promiseIsURIVisited(uri));
+ let visit = placeInfo.visits[0];
+ print("TEST-INFO | test_properties_saved | updatePlaces callback for " +
+ "transition type " + visit.transitionType);
+
+ // Note that TRANSITION_EMBED should not be in the database.
+ const EXPECTED_COUNT = visit.transitionType == TRANSITION_EMBED ? 0 : 1;
+
+ // mozIVisitInfo::date
+ let stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_places h
+ JOIN moz_historyvisits v
+ ON h.id = v.place_id
+ WHERE h.url_hash = hash(:page_url) AND h.url = :page_url
+ AND v.visit_date = :visit_date`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.visit_date = visit.visitDate;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, EXPECTED_COUNT);
+ stmt.finalize();
+
+ // mozIVisitInfo::transitionType
+ stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_places h
+ JOIN moz_historyvisits v
+ ON h.id = v.place_id
+ WHERE h.url_hash = hash(:page_url) AND h.url = :page_url
+ AND v.visit_type = :transition_type`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.transition_type = visit.transitionType;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, EXPECTED_COUNT);
+ stmt.finalize();
+
+ // mozIPlaceInfo::title
+ stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_places h
+ WHERE h.url_hash = hash(:page_url) AND h.url = :page_url
+ AND h.title = :title`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.title = placeInfo.title;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, EXPECTED_COUNT);
+ stmt.finalize();
+
+ // If we have had all of our callbacks, continue running tests.
+ if (++callbackCount == places.length) {
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ }
+ }
+});
+
+add_task(function* test_guid_saved() {
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_saved"),
+ guid: "__TESTGUID__",
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_valid_places_guid(place.guid);
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ let placeInfo = placesResult.results[0];
+ let uri = placeInfo.uri;
+ do_check_true(yield promiseIsURIVisited(uri));
+ do_check_eq(placeInfo.guid, place.guid);
+ do_check_guid_for_uri(uri, place.guid);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_referrer_saved() {
+ let places = [
+ { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/referrer"),
+ visits: [
+ new VisitInfo(),
+ ],
+ },
+ { uri: NetUtil.newURI(TEST_DOMAIN + "test_referrer_saved/test"),
+ visits: [
+ new VisitInfo(),
+ ],
+ },
+ ];
+ places[1].visits[0].referrerURI = places[0].uri;
+ do_check_false(yield promiseIsURIVisited(places[0].uri));
+ do_check_false(yield promiseIsURIVisited(places[1].uri));
+
+ let resultCount = 0;
+ let placesResult = yield promiseUpdatePlaces(places);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ for (let placeInfo of placesResult.results) {
+ let uri = placeInfo.uri;
+ do_check_true(yield promiseIsURIVisited(uri));
+ let visit = placeInfo.visits[0];
+
+ // We need to insert all of our visits before we can test conditions.
+ if (++resultCount == places.length) {
+ do_check_true(places[0].uri.equals(visit.referrerURI));
+
+ let stmt = DBConn().createStatement(
+ `SELECT COUNT(1) AS count
+ FROM moz_historyvisits
+ JOIN moz_places h ON h.id = place_id
+ WHERE url_hash = hash(:page_url) AND url = :page_url
+ AND from_visit = (
+ SELECT v.id
+ FROM moz_historyvisits v
+ JOIN moz_places h ON h.id = place_id
+ WHERE url_hash = hash(:referrer) AND url = :referrer
+ )`
+ );
+ stmt.params.page_url = uri.spec;
+ stmt.params.referrer = visit.referrerURI.spec;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.count, 1);
+ stmt.finalize();
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ }
+ }
+});
+
+add_task(function* test_guid_change_saved() {
+ // First, add a visit for it.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_guid_change_saved"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ // Then, change the guid with visits.
+ place.guid = "_GUIDCHANGE_";
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_guid_for_uri(place.uri, place.guid);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_title_change_saved() {
+ // First, add a visit for it.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_saved"),
+ title: "original title",
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+
+ // Now, make sure the empty string clears the title.
+ place.title = "";
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, null);
+
+ // Then, change the title with visits.
+ place.title = "title change";
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, place.title);
+
+ // Lastly, check that the title is cleared if we set it to null.
+ place.title = null;
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, place.title);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_no_title_does_not_clear_title() {
+ const TITLE = "test title";
+ // First, add a visit for it.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_no_title_does_not_clear_title"),
+ title: TITLE,
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ // Now, make sure that not specifying a title does not clear it.
+ delete place.title;
+ place.visits = [new VisitInfo()];
+ placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+ do_check_title_for_uri(place.uri, TITLE);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_title_change_notifies() {
+ // There are three cases to test. The first case is to make sure we do not
+ // get notified if we do not specify a title.
+ let place = {
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_title_change_notifies"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ let silentObserver =
+ new TitleChangedObserver(place.uri, "DO NOT WANT", function() {
+ do_throw("unexpected callback!");
+ });
+
+ PlacesUtils.history.addObserver(silentObserver, false);
+ let placesResult = yield promiseUpdatePlaces(place);
+ if (placesResult.errors.length > 0) {
+ do_throw("Unexpected error.");
+ }
+
+ // The second case to test is that we get the notification when we add
+ // it for the first time. The first case will fail before our callback if it
+ // is busted, so we can do this now.
+ place.uri = NetUtil.newURI(place.uri.spec + "/new-visit-with-title");
+ place.title = "title 1";
+ function promiseTitleChangedObserver(aPlace) {
+ return new Promise((resolve, reject) => {
+ let callbackCount = 0;
+ let observer = new TitleChangedObserver(aPlace.uri, aPlace.title, function() {
+ switch (++callbackCount) {
+ case 1:
+ // The third case to test is to make sure we get a notification when
+ // we change an existing place.
+ observer.expectedTitle = place.title = "title 2";
+ place.visits = [new VisitInfo()];
+ PlacesUtils.asyncHistory.updatePlaces(place);
+ break;
+ case 2:
+ PlacesUtils.history.removeObserver(silentObserver);
+ PlacesUtils.history.removeObserver(observer);
+ resolve();
+ break;
+ }
+ });
+
+ PlacesUtils.history.addObserver(observer, false);
+ PlacesUtils.asyncHistory.updatePlaces(aPlace);
+ });
+ }
+
+ yield promiseTitleChangedObserver(place);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_visit_notifies() {
+ // There are two observers we need to see for each visit. One is an
+ // nsINavHistoryObserver and the other is the uri-visit-saved observer topic.
+ let place = {
+ guid: "abcdefghijkl",
+ uri: NetUtil.newURI(TEST_DOMAIN + "test_visit_notifies"),
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ do_check_false(yield promiseIsURIVisited(place.uri));
+
+ function promiseVisitObserver(aPlace) {
+ return new Promise((resolve, reject) => {
+ let callbackCount = 0;
+ let finisher = function() {
+ if (++callbackCount == 2) {
+ resolve();
+ }
+ }
+ let visitObserver = new VisitObserver(place.uri, place.guid,
+ function(aVisitDate,
+ aTransitionType) {
+ let visit = place.visits[0];
+ do_check_eq(visit.visitDate, aVisitDate);
+ do_check_eq(visit.transitionType, aTransitionType);
+
+ PlacesUtils.history.removeObserver(visitObserver);
+ finisher();
+ });
+ PlacesUtils.history.addObserver(visitObserver, false);
+ let observer = function(aSubject, aTopic, aData) {
+ do_print("observe(" + aSubject + ", " + aTopic + ", " + aData + ")");
+ do_check_true(aSubject instanceof Ci.nsIURI);
+ do_check_true(aSubject.equals(place.uri));
+
+ Services.obs.removeObserver(observer, URI_VISIT_SAVED);
+ finisher();
+ };
+ Services.obs.addObserver(observer, URI_VISIT_SAVED, false);
+ PlacesUtils.asyncHistory.updatePlaces(place);
+ });
+ }
+
+ yield promiseVisitObserver(place);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+// test with empty mozIVisitInfoCallback object
+add_task(function* test_callbacks_not_supplied() {
+ const URLS = [
+ "imap://cyrus.andrew.cmu.edu/archive.imap", // bad URI
+ "http://mozilla.org/" // valid URI
+ ];
+ let places = [];
+ URLS.forEach(function(url) {
+ try {
+ let place = {
+ uri: NetUtil.newURI(url),
+ title: "test for " + url,
+ visits: [
+ new VisitInfo(),
+ ],
+ };
+ places.push(place);
+ }
+ catch (e) {
+ if (e.result != Cr.NS_ERROR_FAILURE) {
+ throw e;
+ }
+ // NetUtil.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.
+ do_print("Could not construct URI for '" + url + "'; ignoring");
+ }
+ });
+
+ PlacesUtils.asyncHistory.updatePlaces(places, {});
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+// Test that we don't wrongly overwrite typed and hidden when adding new visits.
+add_task(function* test_typed_hidden_not_overwritten() {
+ yield PlacesTestUtils.clearHistory();
+ let places = [
+ { uri: NetUtil.newURI("http://mozilla.org/"),
+ title: "test",
+ visits: [
+ new VisitInfo(TRANSITION_TYPED),
+ new VisitInfo(TRANSITION_LINK)
+ ]
+ },
+ { uri: NetUtil.newURI("http://mozilla.org/"),
+ title: "test",
+ visits: [
+ new VisitInfo(TRANSITION_FRAMED_LINK)
+ ]
+ },
+ ];
+ yield promiseUpdatePlaces(places);
+
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(
+ "SELECT hidden, typed FROM moz_places WHERE url_hash = hash(:url) AND url = :url",
+ { url: "http://mozilla.org/" });
+ Assert.equal(rows[0].getResultByName("typed"), 1,
+ "The page should be marked as typed");
+ Assert.equal(rows[0].getResultByName("hidden"), 0,
+ "The page should be marked as not hidden");
+});
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_async_in_batchmode.js b/toolkit/components/places/tests/unit/test_async_in_batchmode.js
new file mode 100644
index 000000000..b39b26519
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_in_batchmode.js
@@ -0,0 +1,55 @@
+// This is testing the frankenstein situation Sync forces Places into.
+// Sync does runInBatchMode() and before the callback returns the Places async
+// APIs are used (either by Sync itself, or by any other code in the system)
+// As seen in bug 1197856 and bug 1190131.
+
+Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+// This function "waits" for a promise to resolve by spinning a nested event
+// loop.
+function waitForPromise(promise) {
+ let thread = Cc["@mozilla.org/thread-manager;1"].getService().currentThread;
+
+ let finalResult, finalException;
+
+ promise.then(result => {
+ finalResult = result;
+ }, err => {
+ finalException = err;
+ });
+
+ // Keep waiting until our callback is triggered (unless the app is quitting).
+ while (!finalResult && !finalException) {
+ thread.processNextEvent(true);
+ }
+ if (finalException) {
+ throw finalException;
+ }
+ return finalResult;
+}
+
+add_test(function() {
+ let testCompleted = false;
+ PlacesUtils.bookmarks.runInBatchMode({
+ runBatched() {
+ // create a bookmark.
+ let info = { parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/" };
+ let insertPromise = PlacesUtils.bookmarks.insert(info);
+ let bookmark = waitForPromise(insertPromise);
+ // Check we got a bookmark (bookmark creation failed completely in
+ // bug 1190131)
+ equal(bookmark.url, info.url);
+ // Check the promiseItemGuid and promiseItemId helpers - failure in these
+ // was the underlying reason for the failure.
+ let id = waitForPromise(PlacesUtils.promiseItemId(bookmark.guid));
+ let guid = waitForPromise(PlacesUtils.promiseItemGuid(id));
+ equal(guid, bookmark.guid, "id and guid round-tripped correctly");
+ testCompleted = true;
+ }
+ }, null);
+ // make sure we tested what we think we tested.
+ ok(testCompleted);
+ run_next_test();
+});
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 000000000..edc9abf87
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_async_transactions.js
@@ -0,0 +1,1739 @@
+/* -*- 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 tagssvc = PlacesUtils.tagging;
+const annosvc = PlacesUtils.annotations;
+const PT = PlacesTransactions;
+const rootGuid = PlacesUtils.bookmarks.rootGuid;
+
+Components.utils.importGlobalProperties(["URL"]);
+
+// Create and add bookmarks observer.
+var observer = {
+ __proto__: NavBookmarkObserver.prototype,
+
+ tagRelatedGuids: new Set(),
+
+ reset: function () {
+ this.itemsAdded = new Map();
+ this.itemsRemoved = new Map();
+ this.itemsChanged = new Map();
+ this.itemsMoved = new Map();
+ this.beginUpdateBatch = false;
+ this.endUpdateBatch = false;
+ },
+
+ onBeginUpdateBatch: function () {
+ this.beginUpdateBatch = true;
+ },
+
+ onEndUpdateBatch: function () {
+ this.endUpdateBatch = true;
+ },
+
+ onItemAdded:
+ function (aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded,
+ aGuid, aParentGuid) {
+ // Ignore tag items.
+ if (aParentId == PlacesUtils.tagsFolderId ||
+ (aParentId != PlacesUtils.placesRootId &&
+ bmsvc.getFolderIdForItem(aParentId) == PlacesUtils.tagsFolderId)) {
+ this.tagRelatedGuids.add(aGuid);
+ return;
+ }
+
+ this.itemsAdded.set(aGuid, { itemId: aItemId
+ , parentGuid: aParentGuid
+ , index: aIndex
+ , itemType: aItemType
+ , title: aTitle
+ , url: aURI });
+ },
+
+ onItemRemoved:
+ function (aItemId, aParentId, aIndex, aItemType, aURI, aGuid, aParentGuid) {
+ if (this.tagRelatedGuids.has(aGuid))
+ return;
+
+ this.itemsRemoved.set(aGuid, { parentGuid: aParentGuid
+ , index: aIndex
+ , itemType: aItemType });
+ },
+
+ onItemChanged:
+ function (aItemId, aProperty, aIsAnnoProperty, aNewValue, aLastModified,
+ aItemType, aParentId, aGuid, aParentGuid) {
+ if (this.tagRelatedGuids.has(aGuid))
+ return;
+
+ let changesForGuid = this.itemsChanged.get(aGuid);
+ if (changesForGuid === undefined) {
+ changesForGuid = new Map();
+ this.itemsChanged.set(aGuid, changesForGuid);
+ }
+
+ let newValue = aNewValue;
+ if (aIsAnnoProperty) {
+ if (annosvc.itemHasAnnotation(aItemId, aProperty))
+ newValue = annosvc.getItemAnnotation(aItemId, aProperty);
+ else
+ newValue = null;
+ }
+ let change = { isAnnoProperty: aIsAnnoProperty
+ , newValue: newValue
+ , lastModified: aLastModified
+ , itemType: aItemType };
+ changesForGuid.set(aProperty, change);
+ },
+
+ onItemVisited: () => {},
+
+ onItemMoved:
+ function (aItemId, aOldParent, aOldIndex, aNewParent, aNewIndex, aItemType,
+ aGuid, aOldParentGuid, aNewParentGuid) {
+ this.itemsMoved.set(aGuid, { oldParentGuid: aOldParentGuid
+ , oldIndex: aOldIndex
+ , newParentGuid: aNewParentGuid
+ , newIndex: aNewIndex
+ , itemType: aItemType });
+ }
+};
+observer.reset();
+
+// index at which items should begin
+var bmStartIndex = 0;
+
+function run_test() {
+ bmsvc.addObserver(observer, false);
+ do_register_cleanup(function () {
+ bmsvc.removeObserver(observer);
+ });
+
+ run_next_test();
+}
+
+function sanityCheckTransactionHistory() {
+ do_check_true(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)
+ do_check_eq(PT.topUndoEntry, PT.entry(PT.undoPosition));
+ else
+ do_check_null(PT.topUndoEntry);
+ if (PT.undoPosition > 0)
+ do_check_eq(PT.topRedoEntry, PT.entry(PT.undoPosition - 1));
+ else
+ do_check_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();
+ do_check_eq(actualEntries.length, aExpectedEntries.length);
+ do_check_eq(actualUndoPosition, aExpectedUndoPosition);
+
+ function checkEqualEntries(aExpectedEntry, aActualEntry) {
+ do_check_eq(aExpectedEntry.length, aActualEntry.length);
+ aExpectedEntry.forEach( (t, i) => do_check_eq(t, aActualEntry[i]) );
+ }
+ aExpectedEntries.forEach( (e, i) => checkEqualEntries(e, actualEntries[i]) );
+}
+
+function ensureItemsAdded(...items) {
+ Assert.equal(observer.itemsAdded.size, items.length);
+ for (let item of items) {
+ Assert.ok(observer.itemsAdded.has(item.guid));
+ let info = observer.itemsAdded.get(item.guid);
+ Assert.equal(info.parentGuid, item.parentGuid);
+ for (let propName of ["title", "index", "itemType"]) {
+ if (propName in item)
+ Assert.equal(info[propName], item[propName]);
+ }
+ if ("url" in item)
+ Assert.ok(info.url.equals(item.url));
+ }
+}
+
+function ensureItemsRemoved(...items) {
+ Assert.equal(observer.itemsRemoved.size, 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));
+ }
+ else {
+ Assert.ok(observer.itemsRemoved.has(item.guid));
+ let info = observer.itemsRemoved.get(item.guid);
+ Assert.equal(info.parentGuid, item.parentGuid);
+ if ("index" in item)
+ Assert.equal(info.index, item.index);
+ }
+ }
+}
+
+function ensureItemsChanged(...items) {
+ for (let item of items) {
+ do_check_true(observer.itemsChanged.has(item.guid));
+ let changes = observer.itemsChanged.get(item.guid);
+ do_check_true(changes.has(item.property));
+ let info = changes.get(item.property);
+ do_check_eq(info.isAnnoProperty, Boolean(item.isAnnoProperty));
+ do_check_eq(info.newValue, item.newValue);
+ if ("url" in item)
+ do_check_true(item.url.equals(info.url));
+ }
+}
+
+function ensureAnnotationsSet(aGuid, aAnnos) {
+ do_check_true(observer.itemsChanged.has(aGuid));
+ let changes = observer.itemsChanged.get(aGuid);
+ for (let anno of aAnnos) {
+ do_check_true(changes.has(anno.name));
+ let changeInfo = changes.get(anno.name);
+ do_check_true(changeInfo.isAnnoProperty);
+ do_check_eq(changeInfo.newValue, anno.value);
+ }
+}
+
+function ensureItemsMoved(...items) {
+ do_check_true(observer.itemsMoved.size, items.length);
+ for (let item of items) {
+ do_check_true(observer.itemsMoved.has(item.guid));
+ let info = observer.itemsMoved.get(item.guid);
+ do_check_eq(info.oldParentGuid, item.oldParentGuid);
+ do_check_eq(info.oldIndex, item.oldIndex);
+ do_check_eq(info.newParentGuid, item.newParentGuid);
+ do_check_eq(info.newIndex, item.newIndex);
+ }
+}
+
+function ensureTimestampsUpdated(aGuid, aCheckDateAdded = false) {
+ do_check_true(observer.itemsChanged.has(aGuid));
+ let changes = observer.itemsChanged.get(aGuid);
+ if (aCheckDateAdded)
+ do_check_true(changes.has("dateAdded"))
+ do_check_true(changes.has("lastModified"));
+}
+
+function ensureTagsForURI(aURI, aTags) {
+ let tagsSet = tagssvc.getTagsForURI(aURI);
+ do_check_eq(tagsSet.length, aTags.length);
+ do_check_true(aTags.every( t => tagsSet.includes(t)));
+}
+
+function createTestFolderInfo(aTitle = "Test Folder") {
+ return { parentGuid: rootGuid, title: "Test Folder" };
+}
+
+function isLivemarkTree(aTree) {
+ return !!aTree.annos &&
+ aTree.annos.some( a => a.name == PlacesUtils.LMANNO_FEEDURI );
+}
+
+function* ensureLivemarkCreatedByAddLivemark(aLivemarkGuid) {
+ // This throws otherwise.
+ yield PlacesUtils.livemarks.getLivemark({ guid: aLivemarkGuid });
+}
+
+// Checks if two bookmark trees (as returned by promiseBookmarksTree) are the
+// same.
+// false value for aCheckParentAndPosition is ignored if aIsRestoredItem is set.
+function* ensureEqualBookmarksTrees(aOriginal,
+ aNew,
+ aIsRestoredItem = true,
+ aCheckParentAndPosition = 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) {
+ Assert.deepEqual(aOriginal, aNew);
+ if (isLivemarkTree(aNew))
+ yield ensureLivemarkCreatedByAddLivemark(aNew.guid);
+ 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++) {
+ yield ensureEqualBookmarksTrees(aOriginal.children[i],
+ aNew.children[i],
+ false,
+ true);
+ }
+ }
+ 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]);
+ }
+ }
+
+ if (isLivemarkTree(aNew))
+ yield ensureLivemarkCreatedByAddLivemark(aNew.guid);
+}
+
+function* ensureBookmarksTreeRestoredCorrectly(...aOriginalBookmarksTrees) {
+ for (let originalTree of aOriginalBookmarksTrees) {
+ let restoredTree =
+ yield PlacesUtils.promiseBookmarksTree(originalTree.guid);
+ yield ensureEqualBookmarksTrees(originalTree, restoredTree);
+ }
+}
+
+function* ensureNonExistent(...aGuids) {
+ for (let guid of aGuids) {
+ Assert.strictEqual((yield PlacesUtils.promiseBookmarksTree(guid)), null);
+ }
+}
+
+add_task(function* test_recycled_transactions() {
+ function* ensureTransactThrowsFor(aTransaction) {
+ let [txns, undoPosition] = getTransactionsHistoryState();
+ try {
+ yield 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());
+ yield txn_a.transact();
+ ensureUndoState([[txn_a]], 0);
+ yield ensureTransactThrowsFor(txn_a);
+
+ yield PT.undo();
+ ensureUndoState([[txn_a]], 1);
+ ensureTransactThrowsFor(txn_a);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+ ensureTransactThrowsFor(txn_a);
+
+ let txn_b = PT.NewFolder(createTestFolderInfo());
+ yield PT.batch(function* () {
+ try {
+ yield txn_a.transact();
+ do_throw("Shouldn't be able to use the same transaction twice");
+ }
+ catch (ex) { }
+ ensureUndoState();
+ yield txn_b.transact();
+ });
+ ensureUndoState([[txn_b]], 0);
+
+ yield PT.undo();
+ ensureUndoState([[txn_b]], 1);
+ ensureTransactThrowsFor(txn_a);
+ ensureTransactThrowsFor(txn_b);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+ observer.reset();
+});
+
+add_task(function* test_new_folder_with_annotation() {
+ const ANNO = { name: "TestAnno", value: "TestValue" };
+ let folder_info = createTestFolderInfo();
+ folder_info.index = bmStartIndex;
+ folder_info.annotations = [ANNO];
+ ensureUndoState();
+ let txn = PT.NewFolder(folder_info);
+ folder_info.guid = yield txn.transact();
+ let ensureDo = function* (aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ yield ensureItemsAdded(folder_info);
+ ensureAnnotationsSet(folder_info.guid, [ANNO]);
+ if (aRedo)
+ ensureTimestampsUpdated(folder_info.guid, true);
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({ guid: folder_info.guid
+ , parentGuid: folder_info.parentGuid
+ , index: bmStartIndex });
+ observer.reset();
+ };
+
+ yield ensureDo();
+ yield PT.undo();
+ yield ensureUndo();
+ yield PT.redo();
+ yield ensureDo(true);
+ yield PT.undo();
+ ensureUndo();
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_new_bookmark() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test_create_item.com")
+ , index: bmStartIndex
+ , title: "Test creating an item" };
+
+ ensureUndoState();
+ let txn = PT.NewBookmark(bm_info);
+ bm_info.guid = yield txn.transact();
+
+ let ensureDo = function* (aRedo = false) {
+ ensureUndoState([[txn]], 0);
+ yield ensureItemsAdded(bm_info);
+ if (aRedo)
+ ensureTimestampsUpdated(bm_info.guid, true);
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState([[txn]], 1);
+ ensureItemsRemoved({ guid: bm_info.guid
+ , parentGuid: bm_info.parentGuid
+ , index: bmStartIndex });
+ observer.reset();
+ };
+
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo(true);
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_merge_create_folder_and_item() {
+ let folder_info = createTestFolderInfo();
+ let bm_info = { url: NetUtil.newURI("http://test_create_item_to_folder.com")
+ , title: "Test Bookmark"
+ , index: bmStartIndex };
+
+ let [folderTxnResult, bkmTxnResult] = yield PT.batch(function* () {
+ let folderTxn = PT.NewFolder(folder_info);
+ folder_info.guid = bm_info.parentGuid = yield folderTxn.transact();
+ let bkmTxn = PT.NewBookmark(bm_info);
+ bm_info.guid = yield bkmTxn.transact();
+ return [folderTxn, bkmTxn];
+ });
+
+ let ensureDo = function* () {
+ ensureUndoState([[bkmTxnResult, folderTxnResult]], 0);
+ yield ensureItemsAdded(folder_info, bm_info);
+ observer.reset();
+ };
+
+ let ensureUndo = () => {
+ ensureUndoState([[bkmTxnResult, folderTxnResult]], 1);
+ ensureItemsRemoved(folder_info, bm_info);
+ observer.reset();
+ };
+
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo();
+ yield ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_move_items_to_folder() {
+ let folder_a_info = createTestFolderInfo("Folder A");
+ let bkm_a_info = { url: new URL("http://test_move_items.com")
+ , title: "Bookmark A" };
+ let bkm_b_info = { url: NetUtil.newURI("http://test_move_items.com")
+ , title: "Bookmark B" };
+
+ // Test moving items within the same folder.
+ let [folder_a_txn_result, bkm_a_txn_result, bkm_b_txn_result] = yield PT.batch(function* () {
+ let folder_a_txn = PT.NewFolder(folder_a_info);
+
+ folder_a_info.guid = bkm_a_info.parentGuid = bkm_b_info.parentGuid =
+ yield folder_a_txn.transact();
+ let bkm_a_txn = PT.NewBookmark(bkm_a_info);
+ bkm_a_info.guid = yield bkm_a_txn.transact();
+ let bkm_b_txn = PT.NewBookmark(bkm_b_info);
+ bkm_b_info.guid = yield bkm_b_txn.transact();
+ return [folder_a_txn, bkm_a_txn, bkm_b_txn];
+ });
+
+ ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0);
+
+ let moveTxn = PT.Move({ guid: bkm_a_info.guid
+ , newParentGuid: folder_a_info.guid });
+ yield moveTxn.transact();
+
+ let ensureDo = () => {
+ ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 0);
+ ensureItemsMoved({ guid: bkm_a_info.guid
+ , oldParentGuid: folder_a_info.guid
+ , newParentGuid: folder_a_info.guid
+ , oldIndex: 0
+ , newIndex: 1 });
+ observer.reset();
+ };
+ let ensureUndo = () => {
+ ensureUndoState([[moveTxn], [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 1);
+ ensureItemsMoved({ guid: bkm_a_info.guid
+ , oldParentGuid: folder_a_info.guid
+ , newParentGuid: folder_a_info.guid
+ , oldIndex: 1
+ , newIndex: 0 });
+ observer.reset();
+ };
+
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo();
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ yield PT.clearTransactionsHistory(false, true);
+ ensureUndoState([[bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result]], 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 = yield folder_b_txn.transact();
+ ensureUndoState([ [folder_b_txn]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0);
+
+ moveTxn = PT.Move({ guid: bkm_a_info.guid
+ , newParentGuid: folder_b_info.guid
+ , newIndex: bmsvc.DEFAULT_INDEX });
+ yield moveTxn.transact();
+
+ ensureDo = () => {
+ ensureUndoState([ [moveTxn]
+ , [folder_b_txn]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 0);
+ ensureItemsMoved({ guid: bkm_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]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 1);
+ ensureItemsMoved({ guid: bkm_a_info.guid
+ , oldParentGuid: folder_b_info.guid
+ , newParentGuid: folder_a_info.guid
+ , oldIndex: 0
+ , newIndex: 0 });
+ observer.reset();
+ };
+
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+ yield PT.redo();
+ ensureDo();
+ yield PT.undo();
+ ensureUndo();
+
+ // Clean up
+ yield PT.undo(); // folder_b_txn
+ yield PT.undo(); // folder_a_txn + the bookmarks;
+ do_check_eq(observer.itemsRemoved.size, 4);
+ ensureUndoState([ [moveTxn]
+ , [folder_b_txn]
+ , [bkm_b_txn_result, bkm_a_txn_result, folder_a_txn_result] ], 3);
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(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_result,
+ folder_level_2_txn_result] = yield PT.batch(function* () {
+ let folder_level_1_txn = PT.NewFolder(folder_level_1_info);
+ folder_level_1_info.guid = yield folder_level_1_txn.transact();
+ folder_level_2_info.parentGuid = folder_level_1_info.guid;
+ let folder_level_2_txn = PT.NewFolder(folder_level_2_info);
+ folder_level_2_info.guid = yield folder_level_2_txn.transact();
+ return [folder_level_1_txn, folder_level_2_txn];
+ });
+
+ ensureUndoState([[folder_level_2_txn_result, folder_level_1_txn_result]]);
+ yield ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ observer.reset();
+
+ let remove_folder_2_txn = PT.Remove(folder_level_2_info);
+ yield remove_folder_2_txn.transact();
+
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ]);
+ yield ensureItemsRemoved(folder_level_2_info);
+
+ // Undo Remove "Folder Level 2"
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_2_info);
+ ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ yield PT.redo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ]);
+ yield ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo it again
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_2_info);
+ ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ observer.reset();
+
+ // Undo the creation of both folders
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2);
+ yield ensureItemsRemoved(folder_level_2_info, folder_level_1_info);
+ observer.reset();
+
+ // Redo the creation of both folders
+ yield PT.redo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_1_info, folder_level_2_info);
+ ensureTimestampsUpdated(folder_level_1_info.guid, true);
+ ensureTimestampsUpdated(folder_level_2_info.guid, true);
+ observer.reset();
+
+ // Redo Remove "Folder Level 2"
+ yield PT.redo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ]);
+ yield ensureItemsRemoved(folder_level_2_info);
+ observer.reset();
+
+ // Undo everything one last time
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 1);
+ yield ensureItemsAdded(folder_level_2_info);
+ observer.reset();
+
+ yield PT.undo();
+ ensureUndoState([ [remove_folder_2_txn]
+ , [folder_level_2_txn_result, folder_level_1_txn_result] ], 2);
+ yield ensureItemsRemoved(folder_level_2_info, folder_level_2_info);
+ observer.reset();
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_add_and_remove_bookmarks_with_additional_info() {
+ const testURI = NetUtil.newURI("http://add.remove.tag");
+ const TAG_1 = "TestTag1";
+ const TAG_2 = "TestTag2";
+ const KEYWORD = "test_keyword";
+ const POST_DATA = "post_data";
+ const ANNO = { name: "TestAnno", value: "TestAnnoValue" };
+
+ let folder_info = createTestFolderInfo();
+ folder_info.guid = yield 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 = yield PT.NewBookmark(b1_info).transact();
+ ensureTags([TAG_1]);
+ yield PT.undo();
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.redo();
+ ensureTimestampsUpdated(b1_info.guid, true);
+ ensureTags([TAG_1]);
+
+ // Check if the Remove transaction removes and restores tags of children
+ // correctly.
+ yield PT.Remove(folder_info.guid).transact();
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.undo();
+ ensureTimestampsUpdated(b1_info.guid, true);
+ ensureTags([TAG_1]);
+
+ yield PT.redo();
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.undo();
+ ensureTimestampsUpdated(b1_info.guid, true);
+ ensureTags([TAG_1]);
+
+ // * Check that no-op tagging (the uri is already tagged with TAG_1) is
+ // also a no-op on undo.
+ // * Test the "keyword" property of the NewBookmark transaction.
+ observer.reset();
+ let b2_info = { parentGuid: folder_info.guid
+ , url: testURI, tags: [TAG_1, TAG_2]
+ , keyword: KEYWORD
+ , postData: POST_DATA
+ , annotations: [ANNO] };
+ b2_info.guid = yield PT.NewBookmark(b2_info).transact();
+ let b2_post_creation_changes = [
+ { guid: b2_info.guid
+ , isAnnoProperty: true
+ , property: ANNO.name
+ , newValue: ANNO.value },
+ { guid: b2_info.guid
+ , property: "keyword"
+ , newValue: KEYWORD } ];
+ ensureItemsChanged(...b2_post_creation_changes);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ yield PT.undo();
+ yield ensureItemsRemoved(b2_info);
+ ensureTags([TAG_1]);
+
+ // Check if Remove correctly restores keywords, tags and annotations.
+ // Since both bookmarks share the same uri, they also share the keyword that
+ // is not removed along with one of the bookmarks.
+ observer.reset();
+ yield PT.redo();
+ ensureItemsChanged({ guid: b2_info.guid
+ , isAnnoProperty: true
+ , property: ANNO.name
+ , newValue: ANNO.value });
+ ensureTags([TAG_1, TAG_2]);
+
+ // Test Remove for multiple items.
+ observer.reset();
+ yield PT.Remove(b1_info.guid).transact();
+ yield PT.Remove(b2_info.guid).transact();
+ yield PT.Remove(folder_info.guid).transact();
+ yield ensureItemsRemoved(b1_info, b2_info, folder_info);
+ ensureTags([]);
+ // There is no keyword removal notification cause all bookmarks are removed
+ // before the keyword itself, so there's no one to notify.
+ let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null, "keyword has been removed");
+
+ observer.reset();
+ yield PT.undo();
+ yield ensureItemsAdded(folder_info);
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.undo();
+ ensureItemsChanged(...b2_post_creation_changes);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ yield PT.undo();
+ yield ensureItemsAdded(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ // The redo calls below cleanup everything we did.
+ observer.reset();
+ yield PT.redo();
+ yield ensureItemsRemoved(b1_info);
+ ensureTags([TAG_1, TAG_2]);
+
+ observer.reset();
+ yield PT.redo();
+ yield ensureItemsRemoved(b2_info);
+ ensureTags([]);
+
+ observer.reset();
+ yield PT.redo();
+ yield ensureItemsRemoved(folder_info);
+ ensureTags([]);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_creating_and_removing_a_separator() {
+ let folder_info = createTestFolderInfo();
+ let separator_info = {};
+ let undoEntries = [];
+
+ observer.reset();
+ let create_txns = yield PT.batch(function* () {
+ let folder_txn = PT.NewFolder(folder_info);
+ folder_info.guid = separator_info.parentGuid = yield folder_txn.transact();
+ let separator_txn = PT.NewSeparator(separator_info);
+ separator_info.guid = yield separator_txn.transact();
+ return [separator_txn, folder_txn];
+ });
+ undoEntries.unshift(create_txns);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ yield PT.redo();
+ ensureUndoState(undoEntries, 0);
+ ensureItemsAdded(folder_info, separator_info);
+
+ observer.reset();
+ let remove_sep_txn = PT.Remove(separator_info);
+ yield remove_sep_txn.transact();
+ undoEntries.unshift([remove_sep_txn]);
+ ensureUndoState(undoEntries, 0);
+ ensureItemsRemoved(separator_info);
+
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(separator_info);
+
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 2);
+ ensureItemsRemoved(folder_info, separator_info);
+
+ observer.reset();
+ yield PT.redo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsAdded(folder_info, separator_info);
+
+ // Clear redo entries and check that |redo| does nothing
+ observer.reset();
+ yield PT.clearTransactionsHistory(false, true);
+ undoEntries.shift();
+ ensureUndoState(undoEntries, 0);
+ yield PT.redo();
+ ensureItemsAdded();
+ ensureItemsRemoved();
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureUndoState(undoEntries, 1);
+ ensureItemsRemoved(folder_info, separator_info);
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_add_and_remove_livemark() {
+ let createLivemarkTxn = PT.NewLivemark(
+ { feedUrl: NetUtil.newURI("http://test.remove.livemark")
+ , parentGuid: rootGuid
+ , title: "Test Remove Livemark" });
+ let guid = yield createLivemarkTxn.transact();
+ let originalInfo = yield PlacesUtils.promiseBookmarksTree(guid);
+ Assert.ok(originalInfo);
+ yield ensureLivemarkCreatedByAddLivemark(guid);
+
+ let removeTxn = PT.Remove(guid);
+ yield removeTxn.transact();
+ yield ensureNonExistent(guid);
+ function* undo() {
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
+ yield PT.undo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
+ yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ yield PT.undo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
+ yield ensureNonExistent(guid);
+ }
+ function* redo() {
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 2);
+ yield PT.redo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 1);
+ yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ yield PT.redo();
+ ensureUndoState([[removeTxn], [createLivemarkTxn]], 0);
+ yield ensureNonExistent(guid);
+ }
+
+ yield undo();
+ yield redo();
+ yield undo();
+ yield redo();
+
+ // Cleanup
+ yield undo();
+ observer.reset();
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_edit_title() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test_create_item.com")
+ , title: "Original Title" };
+
+ function ensureTitleChange(aCurrentTitle) {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "title"
+ , newValue: aCurrentTitle});
+ }
+
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.EditTitle({ guid: bm_info.guid, title: "New Title" }).transact();
+ ensureTitleChange("New Title");
+
+ observer.reset();
+ yield PT.undo();
+ ensureTitleChange("Original Title");
+
+ observer.reset();
+ yield PT.redo();
+ ensureTitleChange("New Title");
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureTitleChange("Original Title");
+ yield PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_edit_url() {
+ let oldURI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
+ let newURI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
+ let bm_info = { parentGuid: rootGuid, url: oldURI, tags: ["TestTag"] };
+ function ensureURIAndTags(aPreChangeURI, aPostChangeURI, aOLdURITagsPreserved) {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "uri"
+ , newValue: aPostChangeURI.spec });
+ ensureTagsForURI(aPostChangeURI, bm_info.tags);
+ ensureTagsForURI(aPreChangeURI, aOLdURITagsPreserved ? bm_info.tags : []);
+ }
+
+ bm_info.guid = yield 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();
+ yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ yield PT.undo();
+ ensureURIAndTags(newURI, oldURI, false);
+
+ observer.reset();
+ yield PT.redo();
+ ensureURIAndTags(oldURI, newURI, false);
+
+ observer.reset();
+ yield 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 = yield PT.NewBookmark(bm2_info).transact();
+ let bm3_info = Object.create(bm_info);
+ bm3_info.url = newURI;
+ bm3_info.guid = yield PT.NewBookmark(bm3_info).transact();
+
+ observer.reset();
+ yield PT.EditUrl({ guid: bm_info.guid, url: newURI }).transact();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ observer.reset();
+ yield PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+
+ observer.reset();
+ yield PT.redo();
+ ensureURIAndTags(oldURI, newURI, true);
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureURIAndTags(newURI, oldURI, true);
+ yield PT.undo();
+ yield PT.undo();
+ yield PT.undo();
+ ensureItemsRemoved(bm3_info, bm2_info, bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_edit_keyword() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test.edit.keyword") };
+ const KEYWORD = "test_keyword";
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "") {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "keyword"
+ , newValue: aCurrentKeyword });
+ }
+
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.EditKeyword({ guid: bm_info.guid, keyword: KEYWORD, postData: "postData" }).transact();
+ ensureKeywordChange(KEYWORD);
+ let entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData");
+
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange();
+ entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.redo();
+ ensureKeywordChange(KEYWORD);
+ entry = yield PlacesUtils.keywords.fetch(KEYWORD);
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData");
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange();
+ yield PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_edit_specific_keyword() {
+ let bm_info = { parentGuid: rootGuid
+ , url: NetUtil.newURI("http://test.edit.keyword") };
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+ function ensureKeywordChange(aCurrentKeyword = "", aPreviousKeyword = "") {
+ ensureItemsChanged({ guid: bm_info.guid
+ , property: "keyword"
+ , newValue: aCurrentKeyword
+ });
+ }
+
+ yield PlacesUtils.keywords.insert({ keyword: "kw1", url: bm_info.url.spec, postData: "postData1" });
+ yield PlacesUtils.keywords.insert({ keyword: "kw2", url: bm_info.url.spec, postData: "postData2" });
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.EditKeyword({ guid: bm_info.guid, keyword: "keyword", oldKeyword: "kw2" }).transact();
+ ensureKeywordChange("keyword", "kw2");
+ let entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange("kw2", "keyword");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry, null);
+
+ observer.reset();
+ yield PT.redo();
+ ensureKeywordChange("keyword", "kw2");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData1");
+ entry = yield PlacesUtils.keywords.fetch("keyword");
+ Assert.equal(entry.url.href, bm_info.url.spec);
+ Assert.equal(entry.postData, "postData2");
+ entry = yield PlacesUtils.keywords.fetch("kw2");
+ Assert.equal(entry, null);
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureKeywordChange("kw2");
+ yield PT.undo();
+ ensureItemsRemoved(bm_info);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_tag_uri() {
+ // This also tests passing uri specs.
+ let bm_info_a = { url: "http://bookmarked.uri"
+ , parentGuid: rootGuid };
+ let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
+ , parentGuid: rootGuid };
+ let unbookmarked_uri = NetUtil.newURI("http://un.bookmarked.uri");
+
+ function* promiseIsBookmarked(aURI) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncGetBookmarkIds(aURI, ids => {
+ deferred.resolve(ids.length > 0);
+ });
+ return deferred.promise;
+ }
+
+ yield PT.batch(function* () {
+ bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
+ bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
+ });
+
+ function* doTest(aInfo) {
+ let urls = "url" in aInfo ? [aInfo.url] : aInfo.urls;
+ let tags = "tag" in aInfo ? [aInfo.tag] : aInfo.tags;
+
+ let ensureURI = url => typeof(url) == "string" ? NetUtil.newURI(url) : url;
+ urls = urls.map(ensureURI);
+
+ let tagWillAlsoBookmark = new Set();
+ for (let url of urls) {
+ if (!(yield promiseIsBookmarked(url))) {
+ tagWillAlsoBookmark.add(url);
+ }
+ }
+
+ function* ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, tags);
+ Assert.ok(yield promiseIsBookmarked(url));
+ }
+ }
+ function* ensureTagsUnset() {
+ for (let url of urls) {
+ ensureTagsForURI(url, []);
+ if (tagWillAlsoBookmark.has(url))
+ Assert.ok(!(yield promiseIsBookmarked(url)));
+ else
+ Assert.ok(yield promiseIsBookmarked(url));
+ }
+ }
+
+ yield PT.Tag(aInfo).transact();
+ yield ensureTagsSet();
+ yield PT.undo();
+ yield ensureTagsUnset();
+ yield PT.redo();
+ yield ensureTagsSet();
+ yield PT.undo();
+ yield ensureTagsUnset();
+ }
+
+ yield doTest({ url: bm_info_a.url, tags: ["MyTag"] });
+ yield doTest({ urls: [bm_info_a.url], tag: "MyTag" });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A, B"] });
+ yield doTest({ urls: [bm_info_a.url, unbookmarked_uri], tag: "C" });
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_untag_uri() {
+ let bm_info_a = { url: NetUtil.newURI("http://bookmarked.uri")
+ , parentGuid: rootGuid
+ , tags: ["A", "B"] };
+ let bm_info_b = { url: NetUtil.newURI("http://bookmarked2.uri")
+ , parentGuid: rootGuid
+ , tag: "B" };
+
+ yield PT.batch(function* () {
+ bm_info_a.guid = yield PT.NewBookmark(bm_info_a).transact();
+ ensureTagsForURI(bm_info_a.url, bm_info_a.tags);
+ bm_info_b.guid = yield PT.NewBookmark(bm_info_b).transact();
+ ensureTagsForURI(bm_info_b.url, [bm_info_b.tag]);
+ });
+
+ function* doTest(aInfo) {
+ let urls, tagsRemoved;
+ if (aInfo instanceof Ci.nsIURI) {
+ 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(url));
+ }
+
+ function ensureTagsSet() {
+ for (let url of urls) {
+ ensureTagsForURI(url, preRemovalTags.get(url));
+ }
+ }
+ function ensureTagsUnset() {
+ for (let url of urls) {
+ let expectedTags = tagsRemoved.length == 0 ?
+ [] : preRemovalTags.get(url).filter(tag => !tagsRemoved.includes(tag));
+ ensureTagsForURI(url, expectedTags);
+ }
+ }
+
+ yield PT.Untag(aInfo).transact();
+ yield ensureTagsUnset();
+ yield PT.undo();
+ yield ensureTagsSet();
+ yield PT.redo();
+ yield ensureTagsUnset();
+ yield PT.undo();
+ yield ensureTagsSet();
+ }
+
+ yield doTest(bm_info_a);
+ yield doTest(bm_info_b);
+ yield doTest(bm_info_a.url);
+ yield doTest(bm_info_b.url);
+ yield doTest([bm_info_a.url, bm_info_b.url]);
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["A", "B"] });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "B" });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tag: "C" });
+ yield doTest({ urls: [bm_info_a.url, bm_info_b.url], tags: ["C"] });
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureItemsRemoved(bm_info_a, bm_info_b);
+
+ yield PT.clearTransactionsHistory();
+ ensureUndoState();
+});
+
+add_task(function* test_annotate() {
+ let bm_info = { url: NetUtil.newURI("http://test.item.annotation")
+ , parentGuid: rootGuid };
+ let anno_info = { name: "TestAnno", value: "TestValue" };
+ function ensureAnnoState(aSet) {
+ ensureAnnotationsSet(bm_info.guid,
+ [{ name: anno_info.name
+ , value: aSet ? anno_info.value : null }]);
+ }
+
+ bm_info.guid = yield PT.NewBookmark(bm_info).transact();
+
+ observer.reset();
+ yield PT.Annotate({ guid: bm_info.guid, annotation: anno_info }).transact();
+ ensureAnnoState(true);
+
+ observer.reset();
+ yield PT.undo();
+ ensureAnnoState(false);
+
+ observer.reset();
+ yield PT.redo();
+ ensureAnnoState(true);
+
+ // Test removing the annotation by not passing the |value| property.
+ observer.reset();
+ yield PT.Annotate({ guid: bm_info.guid,
+ annotation: { name: anno_info.name }}).transact();
+ ensureAnnoState(false);
+
+ observer.reset();
+ yield PT.undo();
+ ensureAnnoState(true);
+
+ observer.reset();
+ yield PT.redo();
+ ensureAnnoState(false);
+
+ // Cleanup
+ yield PT.undo();
+ observer.reset();
+});
+
+add_task(function* test_annotate_multiple() {
+ let guid = yield PT.NewFolder(createTestFolderInfo()).transact();
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+
+ function AnnoObj(aName, aValue) {
+ this.name = aName;
+ this.value = aValue;
+ this.flags = 0;
+ this.expires = Ci.nsIAnnotationService.EXPIRE_NEVER;
+ }
+
+ function annos(a = null, b = null) {
+ return [new AnnoObj("A", a), new AnnoObj("B", b)];
+ }
+
+ function verifyAnnoValues(a = null, b = null) {
+ let currentAnnos = PlacesUtils.getAnnotationsForItem(itemId);
+ let expectedAnnos = [];
+ if (a !== null)
+ expectedAnnos.push(new AnnoObj("A", a));
+ if (b !== null)
+ expectedAnnos.push(new AnnoObj("B", b));
+
+ Assert.deepEqual(currentAnnos, expectedAnnos);
+ }
+
+ yield PT.Annotate({ guid: guid, annotations: annos(1, 2) }).transact();
+ verifyAnnoValues(1, 2);
+ yield PT.undo();
+ verifyAnnoValues();
+ yield PT.redo();
+ verifyAnnoValues(1, 2);
+
+ yield PT.Annotate({ guid: guid
+ , annotation: { name: "A" } }).transact();
+ verifyAnnoValues(null, 2);
+
+ yield PT.Annotate({ guid: guid
+ , annotation: { name: "B", value: 0 } }).transact();
+ verifyAnnoValues(null, 0);
+ yield PT.undo();
+ verifyAnnoValues(null, 2);
+ yield PT.redo();
+ verifyAnnoValues(null, 0);
+ yield PT.undo();
+ verifyAnnoValues(null, 2);
+ yield PT.undo();
+ verifyAnnoValues(1, 2);
+ yield PT.undo();
+ verifyAnnoValues();
+
+ // Cleanup
+ yield PT.undo();
+ observer.reset();
+});
+
+add_task(function* test_sort_folder_by_name() {
+ let folder_info = createTestFolderInfo();
+
+ let url = NetUtil.newURI("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()];
+ yield PT.batch(function* () {
+ folder_info.guid = yield PT.NewFolder(folder_info).transact();
+ for (let info of originalOrder) {
+ info.parentGuid = folder_info.guid;
+ info.guid = yield info == sep ?
+ PT.NewSeparator(info).transact() :
+ PT.NewBookmark(info).transact();
+ }
+ });
+
+ let folderId = yield PlacesUtils.promiseItemId(folder_info.guid);
+ let folderContainer = PlacesUtils.getFolderContents(folderId).root;
+ function ensureOrder(aOrder) {
+ for (let i = 0; i < folderContainer.childCount; i++) {
+ do_check_eq(folderContainer.getChild(i).bookmarkGuid, aOrder[i].guid);
+ }
+ }
+
+ ensureOrder(originalOrder);
+ yield PT.SortByName(folder_info.guid).transact();
+ ensureOrder(sortedOrder);
+ yield PT.undo();
+ ensureOrder(originalOrder);
+ yield PT.redo();
+ ensureOrder(sortedOrder);
+
+ // Cleanup
+ observer.reset();
+ yield PT.undo();
+ ensureOrder(originalOrder);
+ yield PT.undo();
+ ensureItemsRemoved(...originalOrder, folder_info);
+});
+
+add_task(function* test_livemark_txns() {
+ let livemark_info =
+ { feedUrl: NetUtil.newURI("http://test.feed.uri")
+ , parentGuid: rootGuid
+ , title: "Test Livemark" };
+ function ensureLivemarkAdded() {
+ ensureItemsAdded({ guid: livemark_info.guid
+ , title: livemark_info.title
+ , parentGuid: livemark_info.parentGuid
+ , itemType: bmsvc.TYPE_FOLDER });
+ let annos = [{ name: PlacesUtils.LMANNO_FEEDURI
+ , value: livemark_info.feedUrl.spec }];
+ if ("siteUrl" in livemark_info) {
+ annos.push({ name: PlacesUtils.LMANNO_SITEURI
+ , value: livemark_info.siteUrl.spec });
+ }
+ ensureAnnotationsSet(livemark_info.guid, annos);
+ }
+ function ensureLivemarkRemoved() {
+ ensureItemsRemoved({ guid: livemark_info.guid
+ , parentGuid: livemark_info.parentGuid });
+ }
+
+ function* _testDoUndoRedoUndo() {
+ observer.reset();
+ livemark_info.guid = yield PT.NewLivemark(livemark_info).transact();
+ ensureLivemarkAdded();
+
+ observer.reset();
+ yield PT.undo();
+ ensureLivemarkRemoved();
+
+ observer.reset();
+ yield PT.redo();
+ ensureLivemarkAdded();
+
+ yield PT.undo();
+ ensureLivemarkRemoved();
+ }
+
+ yield* _testDoUndoRedoUndo()
+ livemark_info.siteUrl = NetUtil.newURI("http://feed.site.uri");
+ yield* _testDoUndoRedoUndo();
+
+ // Cleanup
+ observer.reset();
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_copy() {
+ function* duplicate_and_test(aOriginalGuid) {
+ let txn = PT.Copy({ guid: aOriginalGuid, newParentGuid: rootGuid });
+ yield duplicateGuid = yield txn.transact();
+ let originalInfo = yield PlacesUtils.promiseBookmarksTree(aOriginalGuid);
+ let duplicateInfo = yield PlacesUtils.promiseBookmarksTree(duplicateGuid);
+ yield ensureEqualBookmarksTrees(originalInfo, duplicateInfo, false);
+
+ function* redo() {
+ yield PT.redo();
+ yield ensureBookmarksTreeRestoredCorrectly(originalInfo);
+ yield PT.redo();
+ yield ensureBookmarksTreeRestoredCorrectly(duplicateInfo);
+ }
+ function* undo() {
+ yield PT.undo();
+ // also undo the original item addition.
+ yield PT.undo();
+ yield ensureNonExistent(aOriginalGuid, duplicateGuid);
+ }
+
+ yield undo();
+ yield redo();
+ yield undo();
+ yield redo();
+
+ // Cleanup. This also remove the original item.
+ yield PT.undo();
+ observer.reset();
+ yield PT.clearTransactionsHistory();
+ }
+
+ // Test duplicating leafs (bookmark, separator, empty folder)
+ PT.NewBookmark({ url: new URL("http://test.item.duplicate")
+ , parentGuid: rootGuid
+ , annos: [{ name: "Anno", value: "AnnoValue"}] });
+ let sepTxn = PT.NewSeparator({ parentGuid: rootGuid, index: 1 });
+ let livemarkTxn = PT.NewLivemark(
+ { feedUrl: new URL("http://test.feed.uri")
+ , parentGuid: rootGuid
+ , title: "Test Livemark", index: 1 });
+ let emptyFolderTxn = PT.NewFolder(createTestFolderInfo());
+ for (let txn of [livemarkTxn, sepTxn, emptyFolderTxn]) {
+ let guid = yield txn.transact();
+ yield duplicate_and_test(guid);
+ }
+
+ // Test duplicating a folder having some contents.
+ let filledFolderGuid = yield PT.batch(function *() {
+ let folderGuid = yield PT.NewFolder(createTestFolderInfo()).transact();
+ let nestedFolderGuid =
+ yield PT.NewFolder({ parentGuid: folderGuid
+ , title: "Nested Folder" }).transact();
+ // Insert a bookmark under the nested folder.
+ yield PT.NewBookmark({ url: new URL("http://nested.nested.bookmark")
+ , parentGuid: nestedFolderGuid }).transact();
+ // Insert a separator below the nested folder
+ yield PT.NewSeparator({ parentGuid: folderGuid }).transact();
+ // And another bookmark.
+ yield PT.NewBookmark({ url: new URL("http://nested.bookmark")
+ , parentGuid: folderGuid }).transact();
+ return folderGuid;
+ });
+
+ yield duplicate_and_test(filledFolderGuid);
+
+ // Cleanup
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_array_input_for_batch() {
+ let folderTxn = PT.NewFolder(createTestFolderInfo());
+ let folderGuid = yield folderTxn.transact();
+
+ let sep1_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ let sep2_txn = PT.NewSeparator({ parentGuid: folderGuid });
+ yield PT.batch([sep1_txn, sep2_txn]);
+ ensureUndoState([[sep2_txn, sep1_txn], [folderTxn]], 0);
+
+ let ensureChildCount = function* (count) {
+ let tree = yield PlacesUtils.promiseBookmarksTree(folderGuid);
+ if (count == 0)
+ Assert.ok(!("children" in tree));
+ else
+ Assert.equal(tree.children.length, count);
+ };
+
+ yield ensureChildCount(2);
+ yield PT.undo();
+ yield ensureChildCount(0);
+ yield PT.redo()
+ yield ensureChildCount(2);
+ yield PT.undo();
+ yield ensureChildCount(0);
+
+ yield PT.undo();
+ Assert.equal((yield PlacesUtils.promiseBookmarksTree(folderGuid)), null);
+
+ // Cleanup
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_copy_excluding_annotations() {
+ let folderInfo = createTestFolderInfo();
+ let anno = n => { return { name: n, value: 1 } };
+ folderInfo.annotations = [anno("a"), anno("b"), anno("c")];
+ let folderGuid = yield PT.NewFolder(folderInfo).transact();
+
+ let ensureAnnosSet = function* (guid, ...expectedAnnoNames) {
+ let tree = yield PlacesUtils.promiseBookmarksTree(guid);
+ let annoNames = "annos" in tree ?
+ tree.annos.map(a => a.name).sort() : [];
+ Assert.deepEqual(annoNames, expectedAnnoNames);
+ };
+
+ yield ensureAnnosSet(folderGuid, "a", "b", "c");
+
+ let excluding_a_dupeGuid =
+ yield PT.Copy({ guid: folderGuid
+ , newParentGuid: rootGuid
+ , excludingAnnotation: "a" }).transact();
+ yield ensureAnnosSet(excluding_a_dupeGuid, "b", "c");
+
+ let excluding_ac_dupeGuid =
+ yield PT.Copy({ guid: folderGuid
+ , newParentGuid: rootGuid
+ , excludingAnnotations: ["a", "c"] }).transact();
+ yield ensureAnnosSet(excluding_ac_dupeGuid, "b");
+
+ // Cleanup
+ yield PT.undo();
+ yield PT.undo();
+ yield PT.undo();
+ yield PT.clearTransactionsHistory();
+});
+
+add_task(function* test_invalid_uri_spec_throws() {
+ Assert.throws(() =>
+ PT.NewBookmark({ parentGuid: rootGuid
+ , url: "invalid uri spec"
+ , title: "test bookmark" }));
+ Assert.throws(() =>
+ PT.Tag({ tag: "TheTag"
+ , urls: ["invalid uri spec"] }));
+ Assert.throws(() =>
+ PT.Tag({ tag: "TheTag"
+ , urls: ["about:blank", "invalid uri spec"] }));
+});
+
+add_task(function* test_annotate_multiple_items() {
+ let parentGuid = rootGuid;
+ let guids = [
+ yield PT.NewBookmark({ url: "about:blank", parentGuid }).transact(),
+ yield PT.NewFolder({ title: "Test Folder", parentGuid }).transact()];
+
+ let annotation = { name: "TestAnno", value: "TestValue" };
+ yield PT.Annotate({ guids, annotation }).transact();
+
+ function *ensureAnnoSet() {
+ for (let guid of guids) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ Assert.equal(annosvc.getItemAnnotation(itemId, annotation.name),
+ annotation.value);
+ }
+ }
+ function *ensureAnnoUnset() {
+ for (let guid of guids) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ Assert.ok(!annosvc.itemHasAnnotation(itemId, annotation.name));
+ }
+ }
+
+ yield ensureAnnoSet();
+ yield PT.undo();
+ yield ensureAnnoUnset();
+ yield PT.redo();
+ yield ensureAnnoSet();
+ yield PT.undo();
+ yield ensureAnnoUnset();
+
+ // Cleanup
+ yield PT.undo();
+ yield PT.undo();
+ yield ensureNonExistent(...guids);
+ yield PT.clearTransactionsHistory();
+ observer.reset();
+});
+
+add_task(function* test_remove_multiple() {
+ let guids = [];
+ yield PT.batch(function* () {
+ let folderGuid = yield PT.NewFolder({ title: "Test Folder"
+ , parentGuid: rootGuid }).transact();
+ let nestedFolderGuid =
+ yield PT.NewFolder({ title: "Nested Test Folder"
+ , parentGuid: folderGuid }).transact();
+ yield PT.NewSeparator(nestedFolderGuid).transact();
+
+ guids.push(folderGuid);
+
+ let bmGuid =
+ yield PT.NewBookmark({ url: new URL("http://test.bookmark.removed")
+ , parentGuid: rootGuid }).transact();
+ guids.push(bmGuid);
+ });
+
+ let originalInfos = [];
+ for (let guid of guids) {
+ originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid));
+ }
+
+ yield PT.Remove(guids).transact();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Undo the New* transactions batch.
+ yield PT.undo();
+ yield ensureNonExistent(...guids);
+
+ // Redo it.
+ yield PT.redo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Redo remove.
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+
+ // Cleanup
+ yield PT.clearTransactionsHistory();
+ observer.reset();
+});
+
+add_task(function* test_remove_bookmarks_for_urls() {
+ let urls = [new URL("http://test.url.1"), new URL("http://test.url.2")];
+ let guids = [];
+ yield PT.batch(function* () {
+ for (let url of urls) {
+ for (let title of ["test title a", "test title b"]) {
+ let txn = PT.NewBookmark({ url, title, parentGuid: rootGuid });
+ guids.push(yield txn.transact());
+ }
+ }
+ });
+
+ let originalInfos = [];
+ for (let guid of guids) {
+ originalInfos.push(yield PlacesUtils.promiseBookmarksTree(guid));
+ }
+
+ yield PT.RemoveBookmarksForUrls(urls).transact();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+ yield PT.undo();
+ yield ensureBookmarksTreeRestoredCorrectly(...originalInfos);
+
+ // Cleanup.
+ yield PT.redo();
+ yield ensureNonExistent(...guids);
+ yield PT.clearTransactionsHistory();
+ observer.reset();
+});
diff --git a/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js b/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.js
new file mode 100644
index 000000000..7d5df565f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_autocomplete_stopSearch_no_throw.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/. */
+
+/**
+ * Added with bug 508102 to make sure that calling stopSearch on our
+ * AutoComplete implementation does not throw.
+ */
+
+// Globals and Constants
+
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var ac = Cc["@mozilla.org/autocomplete/search;1?name=unifiedcomplete"].
+ getService(Ci.nsIAutoCompleteSearch);
+
+// Test Functions
+
+function test_stopSearch()
+{
+ try {
+ ac.stopSearch();
+ }
+ catch (e) {
+ do_throw("we should not have caught anything!");
+ }
+}
+
+// Test Runner
+
+var tests = [
+ test_stopSearch,
+];
+function run_test()
+{
+ tests.forEach(test => test());
+}
diff --git a/toolkit/components/places/tests/unit/test_bookmark_catobs.js b/toolkit/components/places/tests/unit/test_bookmark_catobs.js
new file mode 100644
index 000000000..e2b589090
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmark_catobs.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ run_next_test()
+}
+
+add_task(function* test_observers() {
+ do_load_manifest("nsDummyObserver.manifest");
+
+ let dummyCreated = false;
+ let dummyReceivedOnItemAdded = false;
+
+ Services.obs.addObserver(function created() {
+ Services.obs.removeObserver(created, "dummy-observer-created");
+ dummyCreated = true;
+ }, "dummy-observer-created", false);
+ Services.obs.addObserver(function added() {
+ Services.obs.removeObserver(added, "dummy-observer-item-added");
+ dummyReceivedOnItemAdded = true;
+ }, "dummy-observer-item-added", false);
+
+ let initialObservers = PlacesUtils.bookmarks.getObservers();
+
+ // Add a common observer, it should be invoked after the category observer.
+ let notificationsPromised = new Promise((resolve, reject) => {
+ PlacesUtils.bookmarks.addObserver( {
+ __proto__: NavBookmarkObserver.prototype,
+ onItemAdded() {
+ let observers = PlacesUtils.bookmarks.getObservers();
+ Assert.equal(observers.length, initialObservers.length + 1);
+
+ // Check the common observer is the last one.
+ for (let i = 0; i < initialObservers.length; ++i) {
+ Assert.equal(initialObservers[i], observers[i]);
+ }
+
+ PlacesUtils.bookmarks.removeObserver(this);
+ observers = PlacesUtils.bookmarks.getObservers();
+ Assert.equal(observers.length, initialObservers.length);
+
+ // Check the category observer has been invoked before this one.
+ Assert.ok(dummyCreated);
+ Assert.ok(dummyReceivedOnItemAdded);
+ resolve();
+ }
+ }, false);
+ });
+
+ // Add a bookmark
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://typed.mozilla.org"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark");
+
+ yield notificationsPromised;
+});
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 000000000..b10dc6185
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html.js
@@ -0,0 +1,385 @@
+/* -*- 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 LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+// 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",
+ description: "folder test comment",
+ dateAdded: 1177541020000000,
+ lastModified: 1177541050000000,
+ children: [
+ { title: "test post keyword",
+ description: "item description",
+ dateAdded: 1177375336000000,
+ lastModified: 1177375423000000,
+ keyword: "test",
+ sidebar: true,
+ 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"
+ }
+ ],
+ unfiled: [
+ { title: "Example.tld",
+ url: "http://example.tld/"
+ }
+ ]
+};
+
+// Pre-Places bookmarks.html file pointer.
+var gBookmarksFileOld;
+// Places bookmarks.html file pointer.
+var gBookmarksFileNew;
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* setup() {
+ // Avoid creating smart bookmarks during the test.
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
+
+ // File pointer to legacy bookmarks file.
+ gBookmarksFileOld = do_get_file("bookmarks.preplaces.html");
+
+ // File pointer to a new Places-exported bookmarks file.
+ gBookmarksFileNew = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ gBookmarksFileNew.append("bookmarks.exported.html");
+ if (gBookmarksFileNew.exists()) {
+ gBookmarksFileNew.remove(false);
+ }
+
+ // 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
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileOld, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_new()
+{
+ // Test importing a Places bookmarks.html file.
+ // 1. import bookmarks.exported.html
+ // 2. run the test-suite
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ yield testImportedBookmarks();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(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
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ const NOTITLE_URL = "http://notitle.mozilla.org/";
+ let bookmark = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: NOTITLE_URL
+ });
+ test_bookmarks.unfiled.push({ title: "", url: NOTITLE_URL });
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+
+ // Cleanup.
+ test_bookmarks.unfiled.pop();
+ // HTML imports don't restore GUIDs yet.
+ let reimportedBookmark = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+ Assert.equal(reimportedBookmark.url.href, bookmark.url.href);
+ yield PlacesUtils.bookmarks.remove(reimportedBookmark);
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(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/information-16.png");
+ const CHROME_FAVICON_URI_2 = NetUtil.newURI("chrome://global/skin/icons/error-16.png");
+
+ do_print("Importing from html");
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Insert bookmark");
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: PAGE_URI,
+ title: "Test"
+ });
+
+ do_print("Set favicon");
+ yield new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ PAGE_URI, CHROME_FAVICON_URI, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ let data = yield 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 });
+
+ do_print("Export to html");
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Set favicon");
+ // Change the favicon to check it's really imported again later.
+ yield new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ PAGE_URI, CHROME_FAVICON_URI_2, true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve, Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ do_print("import from html");
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ do_print("Test imported bookmarks");
+ yield testImportedBookmarks();
+
+ // Cleanup.
+ test_bookmarks.unfiled.pop();
+ // HTML imports don't restore GUIDs yet.
+ let reimportedBookmark = yield PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ });
+ yield PlacesUtils.bookmarks.remove(reimportedBookmark);
+
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(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
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield BookmarkHTMLUtils.exportToFile(gBookmarksFileNew);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ yield BookmarkHTMLUtils.importFromFile(gBookmarksFileNew, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+function* testImportedBookmarks()
+{
+ for (let group in test_bookmarks) {
+ do_print("[testImportedBookmarks()] Checking group '" + group + "'");
+
+ let root;
+ switch (group) {
+ case "menu":
+ root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ break;
+ case "toolbar":
+ root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+ break;
+ case "unfiled":
+ root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ break;
+ }
+
+ let items = test_bookmarks[group];
+ do_check_eq(root.childCount, items.length);
+
+ for (let key in items) {
+ yield checkItem(items[key], root.getChild(key));
+ }
+
+ root.containerOpen = false;
+ }
+}
+
+function* checkItem(aExpected, aNode)
+{
+ let id = aNode.itemId;
+
+ return Task.spawn(function* () {
+ for (prop in aExpected) {
+ switch (prop) {
+ case "type":
+ do_check_eq(aNode.type, aExpected.type);
+ break;
+ case "title":
+ do_check_eq(aNode.title, aExpected.title);
+ break;
+ case "description":
+ do_check_eq(PlacesUtils.annotations
+ .getItemAnnotation(id, DESCRIPTION_ANNO),
+ aExpected.description);
+ break;
+ case "dateAdded":
+ do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(id),
+ aExpected.dateAdded);
+ break;
+ case "lastModified":
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(id),
+ aExpected.lastModified);
+ break;
+ case "url":
+ if (!("feedUrl" in aExpected))
+ do_check_eq(aNode.uri, aExpected.url)
+ break;
+ case "icon":
+ let deferred = Promise.defer();
+ PlacesUtils.favicons.getFaviconDataForPage(
+ NetUtil.newURI(aExpected.url),
+ function (aURI, aDataLen, aData, aMimeType) {
+ deferred.resolve(aData);
+ });
+ let data = yield deferred.promise;
+ let base64Icon = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+ do_check_true(base64Icon == aExpected.icon);
+ break;
+ case "keyword": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.keyword, aExpected.keyword);
+ break;
+ }
+ case "sidebar":
+ do_check_eq(PlacesUtils.annotations
+ .itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ aExpected.sidebar);
+ break;
+ case "postData": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.postData, aExpected.postData);
+ break;
+ }
+ case "charset":
+ let testURI = NetUtil.newURI(aNode.uri);
+ do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset);
+ break;
+ case "feedUrl":
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id });
+ do_check_eq(livemark.siteURI.spec, aExpected.url);
+ do_check_eq(livemark.feedURI.spec, aExpected.feedUrl);
+ break;
+ case "children":
+ let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(folder.hasChildren, aExpected.children.length > 0);
+ folder.containerOpen = true;
+ do_check_eq(folder.childCount, aExpected.children.length);
+
+ for (let index = 0; index < aExpected.children.length; index++) {
+ yield 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 000000000..845b2227b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_corrupt.js
@@ -0,0 +1,143 @@
+/*
+ * This test ensures that importing/exporting to HTML does not stop
+ * if a malformed uri is found.
+ */
+
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+
+const TEST_FAVICON_PAGE_URL = "http://en-US.www.mozilla.com/en-US/firefox/central/";
+const TEST_FAVICON_DATA_SIZE = 580;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_corrupt_file() {
+ // avoid creating the places smart folder during tests
+ Services.prefs.setIntPref("browser.places.smartBookmarksVersion", -1);
+
+ // Import bookmarks from the corrupt file.
+ let corruptHtml = OS.Path.join(do_get_cwd().path, "bookmarks.corrupt.html");
+ yield BookmarkHTMLUtils.importFromFile(corruptHtml, true);
+
+ // Check that bookmarks that are not corrupt have been imported.
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield database_check();
+});
+
+add_task(function* test_corrupt_database() {
+ // Create corruption in the database, then export.
+ let corruptBookmark = yield PlacesUtils.bookmarks.insert({ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://test.mozilla.org",
+ title: "We love belugas" });
+ yield PlacesUtils.withConnectionWrapper("test", Task.async(function*(db) {
+ yield db.execute("UPDATE moz_bookmarks SET fk = NULL WHERE guid = :guid",
+ { guid: corruptBookmark.guid });
+ }));
+
+ let bookmarksFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.exported.html");
+ if ((yield OS.File.exists(bookmarksFile)))
+ yield OS.File.remove(bookmarksFile);
+ yield BookmarkHTMLUtils.exportToFile(bookmarksFile);
+
+ // Import again and check for correctness.
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkHTMLUtils.importFromFile(bookmarksFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield database_check();
+});
+
+/*
+ * Check for imported bookmarks correctness
+ *
+ * @return {Promise}
+ * @resolves When the checks are finished.
+ * @rejects Never.
+ */
+var database_check = Task.async(function* () {
+ // BOOKMARKS MENU
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ Assert.equal(root.childCount, 2);
+
+ let folderNode = root.getChild(1);
+ Assert.equal(folderNode.type, folderNode.RESULT_TYPE_FOLDER);
+ Assert.equal(folderNode.title, "test");
+ Assert.equal(PlacesUtils.bookmarks.getItemDateAdded(folderNode.itemId), 1177541020000000);
+ Assert.equal(PlacesUtils.bookmarks.getItemLastModified(folderNode.itemId), 1177541050000000);
+ Assert.equal("folder test comment",
+ PlacesUtils.annotations.getItemAnnotation(folderNode.itemId,
+ DESCRIPTION_ANNO));
+ // 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 = yield PlacesUtils.keywords.fetch({ url: bookmarkNode.uri });
+ Assert.equal("test", entry.keyword);
+ Assert.equal("hidden1%3Dbar&text1%3D%25s", entry.postData);
+
+ Assert.ok(PlacesUtils.annotations.itemHasAnnotation(bookmarkNode.itemId,
+ LOAD_IN_SIDEBAR_ANNO));
+ Assert.equal(bookmarkNode.dateAdded, 1177375336000000);
+ Assert.equal(bookmarkNode.lastModified, 1177375423000000);
+
+ Assert.equal((yield PlacesUtils.getCharsetForURI(NetUtil.newURI(bookmarkNode.uri))),
+ "ISO-8859-1");
+
+ Assert.equal("item description",
+ PlacesUtils.annotations.getItemAnnotation(bookmarkNode.itemId,
+ DESCRIPTION_ANNO));
+
+ // clean up
+ folderNode.containerOpen = false;
+ root.containerOpen = false;
+
+ // BOOKMARKS TOOLBAR
+ root = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+ Assert.equal(root.childCount, 3);
+
+ // For now some promises are resolved later, so we can't guarantee an order.
+ let foundLivemark = false;
+ for (let i = 0; i < root.childCount; ++i) {
+ let node = root.getChild(i);
+ if (node.title == "Latest Headlines") {
+ foundLivemark = true;
+ Assert.equal("Latest Headlines", node.title);
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ guid: node.bookmarkGuid });
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ livemark.siteURI.spec);
+ Assert.equal("http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml",
+ livemark.feedURI.spec);
+ }
+ }
+ Assert.ok(foundLivemark);
+
+ // cleanup
+ root.containerOpen = false;
+
+ // UNFILED BOOKMARKS
+ root = PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ Assert.equal(root.childCount, 1);
+ root.containerOpen = false;
+
+ // favicons
+ yield 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_import_tags.js b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
new file mode 100644
index 000000000..e4ba433a3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_import_tags.js
@@ -0,0 +1,57 @@
+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(function* test_import_tags() {
+ // Removes bookmarks.html if the file already exists.
+ let HTMLFile = OS.Path.join(OS.Constants.Path.profileDir, "bookmarks.html");
+ if ((yield OS.File.exists(HTMLFile)))
+ yield OS.File.remove(HTMLFile);
+
+ // Adds bookmarks and tags to the database.
+ let bookmarkList = new Set();
+ for (let { uri, title, tags } of bookmarkData) {
+ bookmarkList.add(yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: uri,
+ title }));
+ PlacesUtils.tagging.tagURI(uri, tags);
+ }
+
+ // Exports the bookmarks as a HTML file.
+ yield BookmarkHTMLUtils.exportToFile(HTMLFile);
+
+ // Deletes bookmarks and tags from the database.
+ for (let bookmark of bookmarkList) {
+ yield PlacesUtils.bookmarks.remove(bookmark.guid);
+ }
+
+ // Re-imports the bookmarks from the HTML file.
+ yield BookmarkHTMLUtils.importFromFile(HTMLFile, true);
+
+ // Tests to ensure that the tags are still present for each bookmark URI.
+ for (let { uri, tags } of bookmarkData) {
+ do_print("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_singleframe.js b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
new file mode 100644
index 000000000..02b430ff2
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_html_singleframe.js
@@ -0,0 +1,32 @@
+/* -*- 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
+Cu.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_bookmarks_html_singleframe()
+{
+ let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks_html_singleframe.html");
+ yield BookmarkHTMLUtils.importFromFile(bookmarksFile, true);
+
+ let root = PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ do_check_eq(root.childCount, 1);
+ let folder = root.getChild(0);
+ PlacesUtils.asContainer(folder).containerOpen = true;
+ do_check_eq(folder.title, "Subtitle");
+ do_check_eq(folder.childCount, 1);
+ let bookmark = folder.getChild(0);
+ do_check_eq(bookmark.uri, "http://www.mozilla.org/");
+ do_check_eq(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 000000000..a6801540a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_json.js
@@ -0,0 +1,241 @@
+/* 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/. */
+
+Cu.import("resource://gre/modules/BookmarkJSONUtils.jsm");
+
+function run_test() {
+ run_next_test();
+}
+
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+// 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: "OCyeUO5uu9FK",
+ type: Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR
+ },
+ {
+ guid:"OCyeUO5uu9FL",
+ title: "test",
+ description: "folder test comment",
+ dateAdded: 1177541020000000,
+ // lastModified: 1177541050000000,
+ children: [
+ { guid:"OCyeUO5uu9GX",
+ title: "test post keyword",
+ description: "item description",
+ dateAdded: 1177375336000000,
+ // lastModified: 1177375423000000,
+ keyword: "test",
+ sidebar: true,
+ 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",
+ url: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/livebookmarks/",
+ feedUrl: "http://en-us.fxfeeds.mozilla.com/en-US/firefox/headlines.xml"
+ }
+ ],
+ unfiled: [
+ { guid: "OCyeUO5uu9FW",
+ title: "Example.tld",
+ url: "http://example.tld/"
+ }
+ ]
+};
+
+// Exported bookmarks file pointer.
+var bookmarksExportedFile;
+
+add_task(function* test_import_bookmarks() {
+ let bookmarksFile = OS.Path.join(do_get_cwd().path, "bookmarks.json");
+
+ yield BookmarkJSONUtils.importFromFile(bookmarksFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+});
+
+add_task(function* test_export_bookmarks() {
+ bookmarksExportedFile = OS.Path.join(OS.Constants.Path.profileDir,
+ "bookmarks.exported.json");
+ yield BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+});
+
+add_task(function* test_import_exported_bookmarks() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+});
+
+add_task(function* test_import_ontop() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield BookmarkJSONUtils.exportToFile(bookmarksExportedFile);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield BookmarkJSONUtils.importFromFile(bookmarksExportedFile, true);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ yield testImportedBookmarks();
+});
+
+add_task(function* test_clean() {
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+function* testImportedBookmarks() {
+ for (let group in test_bookmarks) {
+ do_print("[testImportedBookmarks()] Checking group '" + group + "'");
+
+ let root;
+ switch (group) {
+ case "menu":
+ root =
+ PlacesUtils.getFolderContents(PlacesUtils.bookmarksMenuFolderId).root;
+ break;
+ case "toolbar":
+ root =
+ PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root;
+ break;
+ case "unfiled":
+ root =
+ PlacesUtils.getFolderContents(PlacesUtils.unfiledBookmarksFolderId).root;
+ break;
+ }
+
+ let items = test_bookmarks[group];
+ do_check_eq(root.childCount, items.length);
+
+ for (let key in items) {
+ yield checkItem(items[key], root.getChild(key));
+ }
+
+ root.containerOpen = false;
+ }
+}
+
+function* checkItem(aExpected, aNode) {
+ let id = aNode.itemId;
+
+ return Task.spawn(function* () {
+ for (prop in aExpected) {
+ switch (prop) {
+ case "type":
+ do_check_eq(aNode.type, aExpected.type);
+ break;
+ case "title":
+ do_check_eq(aNode.title, aExpected.title);
+ break;
+ case "description":
+ do_check_eq(PlacesUtils.annotations.getItemAnnotation(
+ id, DESCRIPTION_ANNO), aExpected.description);
+ break;
+ case "dateAdded":
+ do_check_eq(PlacesUtils.bookmarks.getItemDateAdded(id),
+ aExpected.dateAdded);
+ break;
+ case "lastModified":
+ do_check_eq(PlacesUtils.bookmarks.getItemLastModified(id),
+ aExpected.lastModified);
+ break;
+ case "url":
+ if (!("feedUrl" in aExpected))
+ do_check_eq(aNode.uri, aExpected.url);
+ break;
+ case "icon":
+ let deferred = Promise.defer();
+ PlacesUtils.favicons.getFaviconDataForPage(
+ NetUtil.newURI(aExpected.url),
+ function (aURI, aDataLen, aData, aMimeType) {
+ deferred.resolve(aData);
+ });
+ let data = yield deferred.promise;
+ let base64Icon = "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, data));
+ do_check_true(base64Icon == aExpected.icon);
+ break;
+ case "keyword": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.keyword, aExpected.keyword);
+ break;
+ }
+ case "guid":
+ let guid = yield PlacesUtils.promiseItemGuid(id);
+ do_check_eq(guid, aExpected.guid);
+ break;
+ case "sidebar":
+ do_check_eq(PlacesUtils.annotations.itemHasAnnotation(
+ id, LOAD_IN_SIDEBAR_ANNO), aExpected.sidebar);
+ break;
+ case "postData": {
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ Assert.equal(entry.postData, aExpected.postData);
+ break;
+ }
+ case "charset":
+ let testURI = NetUtil.newURI(aNode.uri);
+ do_check_eq((yield PlacesUtils.getCharsetForURI(testURI)), aExpected.charset);
+ break;
+ case "feedUrl":
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ id: id });
+ do_check_eq(livemark.siteURI.spec, aExpected.url);
+ do_check_eq(livemark.feedURI.spec, aExpected.feedUrl);
+ break;
+ case "children":
+ let folder = aNode.QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(folder.hasChildren, aExpected.children.length > 0);
+ folder.containerOpen = true;
+ do_check_eq(folder.childCount, aExpected.children.length);
+
+ for (let index = 0; index < aExpected.children.length; index++) {
+ yield 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_restore_notification.js b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
new file mode 100644
index 000000000..2f8022c6b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_restore_notification.js
@@ -0,0 +1,325 @@
+/* -*- 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/. */
+
+Cu.import("resource://gre/modules/BookmarkHTMLUtils.jsm");
+
+/**
+ * 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|.
+ */
+function* addBookmarks() {
+ for (let url of uris) {
+ yield PlacesUtils.bookmarks.insert({
+ url: url, parentGuid: PlacesUtils.bookmarks.menuGuid
+ })
+ }
+ checkBookmarksExist();
+}
+
+/**
+ * Checks that all of the bookmarks created for |uris| exist. It works by
+ * creating one query per URI and then ORing all the queries. The number of
+ * results returned should be uris.length.
+ */
+function checkBookmarksExist() {
+ let hs = PlacesUtils.history;
+ let queries = uris.map(function (u) {
+ let q = hs.getNewQuery();
+ q.uri = uri(u);
+ return q;
+ });
+ let options = hs.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ let root = hs.executeQueries(queries, uris.length, options).root;
+ root.containerOpen = true;
+ Assert.equal(root.childCount, uris.length);
+ root.containerOpen = false;
+}
+
+/**
+ * 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
+ */
+function promiseFile(aBasename) {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, aBasename);
+ do_print("opening " + path);
+ return OS.File.open(path, { truncate: true })
+ .then(aFile => {
+ aFile.close();
+ 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
+ */
+function* checkObservers(expectPromises, expectedData) {
+ let [promiseBegin, promiseResult] = expectPromises;
+
+ let beginData = (yield promiseBegin)[1];
+ Assert.equal(beginData, expectedData.data,
+ "Data for current test should be what is expected");
+
+ let [resultSubject, resultData] = yield 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(aSubject.QueryInterface(Ci.nsISupportsPRInt64).data,
+ expectedData.folderId);
+ } else {
+ Assert.equal(expectedData.folderId, null);
+ }
+}
+
+/**
+ * Run after every test cases.
+ */
+function* teardown(file, begin, success, fail) {
+ // On restore failed, file may not exist, so wrap in try-catch.
+ try {
+ yield OS.File.remove(file, {ignoreAbsent: true});
+ } catch (e) {}
+
+ // clean up bookmarks
+ yield PlacesUtils.bookmarks.eraseEverything();
+}
+
+add_task(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);
+
+ do_print("JSON restore: normal restore should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.json");
+ yield addBookmarks();
+
+ yield BookmarkJSONUtils.exportToFile(file);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ try {
+ yield BookmarkJSONUtils.importFromFile(file, true);
+ } catch (e) {
+ do_throw(" Restore should not have failed" + e);
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_json_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("JSON restore: empty file should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.json");
+ try {
+ yield BookmarkJSONUtils.importFromFile(file, true);
+ } catch (e) {
+ do_throw(" Restore should not have failed" + e);
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_json_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_JSON,
+ folderId: null
+ }
+ let expectPromises = registerObservers(false);
+
+ do_print("JSON restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ file.append("this file doesn't exist because nobody created it 1");
+ try {
+ yield BookmarkJSONUtils.importFromFile(file, true);
+ do_throw(" Restore should have failed");
+ } catch (e) {}
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_restore_normal() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML restore: normal restore should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.html");
+ yield addBookmarks();
+ yield BookmarkHTMLUtils.exportToFile(file);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ try {
+ BookmarkHTMLUtils.importFromFile(file, false)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML restore: empty file should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.init.html");
+ try {
+ BookmarkHTMLUtils.importFromFile(file, false)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML,
+ folderId: null
+ }
+ let expectPromises = registerObservers(false);
+
+ do_print("HTML restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ file.append("this file doesn't exist because nobody created it 2");
+ try {
+ yield BookmarkHTMLUtils.importFromFile(file, false);
+ do_throw("Should fail!");
+ } catch (e) {}
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_init_restore_normal() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML initial restore: normal restore should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.init.html");
+ yield addBookmarks();
+ yield BookmarkHTMLUtils.exportToFile(file);
+ yield PlacesUtils.bookmarks.eraseEverything();
+ try {
+ BookmarkHTMLUtils.importFromFile(file, true)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_init_restore_empty() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null
+ }
+ let expectPromises = registerObservers(true);
+
+ do_print("HTML initial restore: empty file should succeed");
+ let file = yield promiseFile("bookmarks-test_restoreNotification.init.html");
+ try {
+ BookmarkHTMLUtils.importFromFile(file, true)
+ .then(null, do_report_unexpected_exception);
+ } catch (e) {
+ do_throw(" Restore should not have failed");
+ }
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
+
+add_task(function* test_html_init_restore_nonexist() {
+ let expectedData = {
+ data: NSIOBSERVER_DATA_HTML_INIT,
+ folderId: null
+ }
+ let expectPromises = registerObservers(false);
+
+ do_print("HTML initial restore: nonexistent file should fail");
+ let file = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ file.append("this file doesn't exist because nobody created it 3");
+ try {
+ yield BookmarkHTMLUtils.importFromFile(file, true);
+ do_throw("Should fail!");
+ } catch (e) {}
+
+ yield checkObservers(expectPromises, expectedData);
+ yield teardown(file);
+});
diff --git a/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
new file mode 100644
index 000000000..959dfe85f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bookmarks_setNullTitle.js
@@ -0,0 +1,44 @@
+/* -*- 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/. */
+
+/**
+ * Both SetItemtitle and insertBookmark should allow for null titles.
+ */
+
+const bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+const TEST_URL = "http://www.mozilla.org";
+
+function run_test() {
+ // Insert a bookmark with an empty title.
+ var itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri(TEST_URL),
+ bs.DEFAULT_INDEX,
+ "");
+ // Check returned title is an empty string.
+ do_check_eq(bs.getItemTitle(itemId), "");
+ // Set title to null.
+ bs.setItemTitle(itemId, null);
+ // Check returned title is null.
+ do_check_eq(bs.getItemTitle(itemId), null);
+ // Cleanup.
+ bs.removeItem(itemId);
+
+ // Insert a bookmark with a null title.
+ itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri(TEST_URL),
+ bs.DEFAULT_INDEX,
+ null);
+ // Check returned title is null.
+ do_check_eq(bs.getItemTitle(itemId), null);
+ // Set title to an empty string.
+ bs.setItemTitle(itemId, "");
+ // Check returned title is an empty string.
+ do_check_eq(bs.getItemTitle(itemId), "");
+ // Cleanup.
+ bs.removeItem(itemId);
+}
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 000000000..b67e141e6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_broken_folderShortcut_result.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("http://1.moz.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 1"
+ );
+ let id1 = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("place:folder=1234"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Shortcut 1"
+ );
+ let id2 = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("place:folder=-1"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Shortcut 2"
+ );
+ PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, NetUtil.newURI("http://2.moz.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 2"
+ );
+
+ // Add also a simple visit.
+ yield PlacesTestUtils.addVisits(uri(("http://3.moz.org/")));
+
+ // Query containing a broken folder shortcuts among results.
+ let query = PlacesUtils.history.getNewQuery();
+ query.setFolders([PlacesUtils.unfiledBookmarksFolderId], 1);
+ let options = PlacesUtils.history.getNewQueryOptions();
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+
+ do_check_eq(root.childCount, 4);
+
+ let shortcut = root.getChild(1);
+ do_check_eq(shortcut.uri, "place:folder=1234");
+ PlacesUtils.asContainer(shortcut);
+ shortcut.containerOpen = true;
+ do_check_eq(shortcut.childCount, 0);
+ shortcut.containerOpen = false;
+ // Remove the broken shortcut while the containing result is open.
+ PlacesUtils.bookmarks.removeItem(id1);
+ do_check_eq(root.childCount, 3);
+
+ shortcut = root.getChild(1);
+ do_check_eq(shortcut.uri, "place:folder=-1");
+ PlacesUtils.asContainer(shortcut);
+ shortcut.containerOpen = true;
+ do_check_eq(shortcut.childCount, 0);
+ shortcut.containerOpen = false;
+ // Remove the broken shortcut while the containing result is open.
+ PlacesUtils.bookmarks.removeItem(id2);
+ do_check_eq(root.childCount, 2);
+
+ root.containerOpen = false;
+
+ // Broken folder shortcut as root node.
+ query = PlacesUtils.history.getNewQuery();
+ query.setFolders([1234], 1);
+ options = PlacesUtils.history.getNewQueryOptions();
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 0);
+ root.containerOpen = false;
+
+ // Broken folder shortcut as root node with folder=-1.
+ query = PlacesUtils.history.getNewQuery();
+ query.setFolders([-1], 1);
+ options = PlacesUtils.history.getNewQueryOptions();
+ root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(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 000000000..5f88c26e3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_browserhistory.js
@@ -0,0 +1,129 @@
+/* -*- 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 = NetUtil.newURI("http://mozilla.com/");
+const TEST_SUBDOMAIN_URI = NetUtil.newURI("http://foobar.mozilla.com/");
+
+add_task(function* test_addPage() {
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePage() {
+ PlacesUtils.bhistory.removePage(TEST_URI);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePages() {
+ let pages = [];
+ for (let i = 0; i < 8; i++) {
+ pages.push(NetUtil.newURI(TEST_URI.spec + i));
+ }
+
+ yield PlacesTestUtils.addVisits(pages.map(uri => ({ 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;
+ PlacesUtils.annotations.setPageAnnotation(pages[ANNO_INDEX],
+ ANNO_NAME, ANNO_VALUE, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ pages[BOOKMARK_INDEX],
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test bookmark");
+ PlacesUtils.annotations.setPageAnnotation(pages[BOOKMARK_INDEX],
+ ANNO_NAME, ANNO_VALUE, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+
+ PlacesUtils.bhistory.removePages(pages, pages.length);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+
+ // Check that the bookmark and its annotation still exist.
+ do_check_true(PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId, 0) > 0);
+ do_check_eq(PlacesUtils.annotations.getPageAnnotation(pages[BOOKMARK_INDEX], ANNO_NAME),
+ ANNO_VALUE);
+
+ // Check the annotation on the non-bookmarked page does not exist anymore.
+ try {
+ PlacesUtils.annotations.getPageAnnotation(pages[ANNO_INDEX], ANNO_NAME);
+ do_throw("did not expire expire_never anno on a not bookmarked item");
+ } catch (ex) {}
+
+ // Cleanup.
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_removePagesByTimeframe() {
+ let visits = [];
+ let startDate = (Date.now() - 10000) * 1000;
+ for (let i = 0; i < 10; i++) {
+ visits.push({
+ uri: NetUtil.newURI(TEST_URI.spec + i),
+ visitDate: startDate + i * 1000
+ });
+ }
+
+ yield PlacesTestUtils.addVisits(visits);
+
+ // Delete all pages except the first and the last.
+ PlacesUtils.bhistory.removePagesByTimeframe(startDate + 1000, startDate + 8000);
+
+ // Check that we have removed the correct pages.
+ for (let i = 0; i < 10; i++) {
+ do_check_eq(page_in_database(NetUtil.newURI(TEST_URI.spec + i)) == 0,
+ i > 0 && i < 9);
+ }
+
+ // Clear remaining items and check that all pages have been removed.
+ PlacesUtils.bhistory.removePagesByTimeframe(startDate, startDate + 9000);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePagesFromHost() {
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ PlacesUtils.bhistory.removePagesFromHost("mozilla.com", true);
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_removePagesFromHost_keepSubdomains() {
+ yield PlacesTestUtils.addVisits([{ uri: TEST_URI }, { uri: TEST_SUBDOMAIN_URI }]);
+ PlacesUtils.bhistory.removePagesFromHost("mozilla.com", false);
+ do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_history_clear() {
+ yield PlacesTestUtils.clearHistory();
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+});
+
+add_task(function* test_getObservers() {
+ // Ensure that getObservers() invalidates the hasHistoryEntries cache.
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ do_check_eq(1, PlacesUtils.history.hasHistoryEntries);
+ // This is just for testing purposes, never do it.
+ return new Promise((resolve, reject) => {
+ DBConn().executeSimpleSQLAsync("DELETE FROM moz_historyvisits", {
+ handleError: function(error) {
+ reject(error);
+ },
+ handleResult: function(result) {
+ },
+ handleCompletion: function(result) {
+ // Just invoking getObservers should be enough to invalidate the cache.
+ PlacesUtils.history.getObservers();
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+ resolve();
+ }
+ });
+ });
+});
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js b/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js
new file mode 100644
index 000000000..a7ad1257a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_bug636917_isLivemark.js
@@ -0,0 +1,35 @@
+// Test that asking for a livemark in a annotationChanged notification works.
+add_task(function* () {
+ let annoPromise = new Promise(resolve => {
+ let annoObserver = {
+ onItemAnnotationSet(id, name) {
+ if (name == PlacesUtils.LMANNO_FEEDURI) {
+ PlacesUtils.annotations.removeObserver(this);
+ resolve();
+ }
+ },
+ onItemAnnotationRemoved() {},
+ onPageAnnotationSet() {},
+ onPageAnnotationRemoved() {},
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAnnotationObserver
+ ]),
+ };
+ PlacesUtils.annotations.addObserver(annoObserver, false);
+ });
+
+
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "livemark title"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , index: PlacesUtils.bookmarks.DEFAULT_INDEX
+ , siteURI: uri("http://example.com/")
+ , feedURI: uri("http://example.com/rdf")
+ });
+
+ yield annoPromise;
+
+ livemark = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ Assert.ok(livemark);
+ yield PlacesUtils.livemarks.removeLivemark({ guid: livemark.guid });
+});
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 000000000..4c3e38fa4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_childlessTags.js
@@ -0,0 +1,117 @@
+/* -*- 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.
+ */
+
+// Add your tests here. Each is an object with a summary string |desc| and a
+// method run() that's called to run the test.
+var tests = [
+ {
+ desc: "Removing a tagged bookmark should cause the tag to be removed.",
+ run: function () {
+ print(" Make a bookmark.");
+ var bmId = bmsvc.insertBookmark(bmsvc.unfiledBookmarksFolder,
+ BOOKMARK_URI,
+ bmsvc.DEFAULT_INDEX,
+ "test bookmark");
+ do_check_true(bmId > 0);
+
+ print(" Tag it up.");
+ var tags = ["foo", "bar"];
+ tagssvc.tagURI(BOOKMARK_URI, tags);
+ ensureTagsExist(tags);
+
+ print(" Remove the bookmark. The tags should no longer exist.");
+ bmsvc.removeItem(bmId);
+ ensureTagsExist([]);
+ }
+ },
+
+ {
+ desc: "Removing a folder containing a tagged bookmark should cause the " +
+ "tag to be removed.",
+ run: function () {
+ print(" Make a folder.");
+ var folderId = bmsvc.createFolder(bmsvc.unfiledBookmarksFolder,
+ "test folder",
+ bmsvc.DEFAULT_INDEX);
+ do_check_true(folderId > 0);
+
+ print(" Stick a bookmark in the folder.");
+ var bmId = bmsvc.insertBookmark(folderId,
+ BOOKMARK_URI,
+ bmsvc.DEFAULT_INDEX,
+ "test bookmark");
+ do_check_true(bmId > 0);
+
+ print(" Tag the bookmark.");
+ var tags = ["foo", "bar"];
+ tagssvc.tagURI(BOOKMARK_URI, tags);
+ ensureTagsExist(tags);
+
+ print(" Remove the folder. The tags should no longer exist.");
+ bmsvc.removeItem(folderId);
+ ensureTagsExist([]);
+ }
+ }
+];
+
+var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+
+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/");
+
+/**
+ * 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_TAG_QUERY;
+ 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|.
+ do_check_eq(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);
+ do_check_true(indexOfTag >= 0);
+ tags.splice(indexOfTag, 1);
+ }
+
+ resultRoot.containerOpen = false;
+}
+
+function run_test()
+{
+ tests.forEach(function (test) {
+ print("Running test: " + test.desc);
+ test.run();
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_corrupt_telemetry.js b/toolkit/components/places/tests/unit/test_corrupt_telemetry.js
new file mode 100644
index 000000000..cd9e9ec0c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_corrupt_telemetry.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that history initialization correctly handles a request to forcibly
+// replace the current database.
+
+add_task(function* () {
+ let profileDBPath = yield OS.Path.join(OS.Constants.Path.profileDir, "places.sqlite");
+ yield OS.File.remove(profileDBPath, {ignoreAbsent: true});
+ // Ensure that our database doesn't already exist.
+ Assert.ok(!(yield OS.File.exists(profileDBPath)), "places.sqlite shouldn't exist");
+ let dir = yield OS.File.getCurrentDirectory();
+ let src = OS.Path.join(dir, "corruptDB.sqlite");
+ yield OS.File.copy(src, profileDBPath);
+ Assert.ok(yield OS.File.exists(profileDBPath), "places.sqlite should exist");
+
+ let count = Services.telemetry
+ .getHistogramById("PLACES_DATABASE_CORRUPTION_HANDLING_STAGE")
+ .snapshot()
+ .counts[3];
+ Assert.equal(count, 0, "There should be no telemetry");
+
+ do_check_eq(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ count = Services.telemetry
+ .getHistogramById("PLACES_DATABASE_CORRUPTION_HANDLING_STAGE")
+ .snapshot()
+ .counts[3];
+ Assert.equal(count, 1, "Telemetry should have been added");
+});
diff --git a/toolkit/components/places/tests/unit/test_crash_476292.js b/toolkit/components/places/tests/unit/test_crash_476292.js
new file mode 100644
index 000000000..8f0862022
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_crash_476292.js
@@ -0,0 +1,28 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This tests a crash during startup found in bug 476292 that was caused by
+ * getting the bookmarks service during nsNavHistory::Init when the bookmarks
+ * service was created before the history service was.
+ */
+
+function run_test()
+{
+ // First, we need to move our old database file into our test profile
+ // directory. This will trigger DATABASE_STATUS_UPGRADED (CREATE is not
+ // sufficient since there will be no entries to update frecencies for, which
+ // causes us to get the bookmarks service in the first place).
+ let dbFile = do_get_file("bug476292.sqlite");
+ let profD = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).
+ get(NS_APP_USER_PROFILE_50_DIR, Ci.nsIFile);
+ dbFile.copyTo(profD, "places.sqlite");
+
+ // Now get the bookmarks service. This will crash when the bug exists.
+ Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+}
diff --git a/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js b/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
new file mode 100644
index 000000000..e83d0fdae
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_database_replaceOnStartup.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests that history initialization correctly handles a request to forcibly
+// replace the current database.
+
+function run_test() {
+ // Ensure that our database doesn't already exist.
+ let dbFile = gProfD.clone();
+ dbFile.append("places.sqlite");
+ do_check_false(dbFile.exists());
+
+ dbFile = gProfD.clone();
+ dbFile.append("places.sqlite.corrupt");
+ do_check_false(dbFile.exists());
+
+ let file = do_get_file("default.sqlite");
+ file.copyToFollowingLinks(gProfD, "places.sqlite");
+ file = gProfD.clone();
+ file.append("places.sqlite");
+
+ // Create some unique stuff to check later.
+ let db = Services.storage.openUnsharedDatabase(file);
+ db.executeSimpleSQL("CREATE TABLE test (id INTEGER PRIMARY KEY)");
+ db.close();
+
+ Services.prefs.setBoolPref("places.database.replaceOnStartup", true);
+ do_check_eq(PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT);
+
+ dbFile = gProfD.clone();
+ dbFile.append("places.sqlite");
+ do_check_true(dbFile.exists());
+
+ // Check the new database is really a new one.
+ db = Services.storage.openUnsharedDatabase(file);
+ try {
+ db.executeSimpleSQL("DELETE * FROM test");
+ do_throw("The new database should not have our unique content");
+ } catch (ex) {}
+ db.close();
+
+ dbFile = gProfD.clone();
+ dbFile.append("places.sqlite.corrupt");
+ do_check_true(dbFile.exists());
+}
diff --git a/toolkit/components/places/tests/unit/test_download_history.js b/toolkit/components/places/tests/unit/test_download_history.js
new file mode 100644
index 000000000..643360b20
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_download_history.js
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the nsIDownloadHistory Places implementation.
+ */
+
+XPCOMUtils.defineLazyServiceGetter(this, "gDownloadHistory",
+ "@mozilla.org/browser/download-history;1",
+ "nsIDownloadHistory");
+
+const DOWNLOAD_URI = NetUtil.newURI("http://www.example.com/");
+const REFERRER_URI = NetUtil.newURI("http://www.example.org/");
+const PRIVATE_URI = NetUtil.newURI("http://www.example.net/");
+
+/**
+ * Waits for the first visit notification to be received.
+ *
+ * @param aCallback
+ * Callback function to be called with the same arguments of onVisit.
+ */
+function waitForOnVisit(aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onVisit: function HO_onVisit() {
+ PlacesUtils.history.removeObserver(this);
+ aCallback.apply(null, arguments);
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+/**
+ * Waits for the first onDeleteURI notification to be received.
+ *
+ * @param aCallback
+ * Callback function to be called with the same arguments of onDeleteURI.
+ */
+function waitForOnDeleteURI(aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onDeleteURI: function HO_onDeleteURI() {
+ PlacesUtils.history.removeObserver(this);
+ aCallback.apply(null, arguments);
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+/**
+ * Waits for the first onDeleteVisits notification to be received.
+ *
+ * @param aCallback
+ * Callback function to be called with the same arguments of onDeleteVisits.
+ */
+function waitForOnDeleteVisits(aCallback) {
+ let historyObserver = {
+ __proto__: NavHistoryObserver.prototype,
+ onDeleteVisits: function HO_onDeleteVisits() {
+ PlacesUtils.history.removeObserver(this);
+ aCallback.apply(null, arguments);
+ }
+ };
+ PlacesUtils.history.addObserver(historyObserver, false);
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_test(function test_dh_is_from_places()
+{
+ // Test that this nsIDownloadHistory is the one places implements.
+ do_check_true(gDownloadHistory instanceof Ci.mozIAsyncHistory);
+
+ run_next_test();
+});
+
+add_test(function test_dh_addRemoveDownload()
+{
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+
+ // Verify that the URI is already available in results at this time.
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ waitForOnDeleteURI(function DHRAD_onDeleteURI(aDeletedURI) {
+ do_check_true(aDeletedURI.equals(DOWNLOAD_URI));
+
+ // Verify that the URI is already available in results at this time.
+ do_check_false(!!page_in_database(DOWNLOAD_URI));
+
+ run_next_test();
+ });
+ gDownloadHistory.removeAllDownloads();
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+});
+
+add_test(function test_dh_addMultiRemoveDownload()
+{
+ PlacesTestUtils.addVisits({
+ uri: DOWNLOAD_URI,
+ transition: TRANSITION_TYPED
+ }).then(function () {
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aDeletedURI) {
+ do_check_true(aDeletedURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+ gDownloadHistory.removeAllDownloads();
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+ });
+});
+
+add_test(function test_dh_addBookmarkRemoveDownload()
+{
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ DOWNLOAD_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A bookmark");
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ waitForOnDeleteVisits(function DHRAD_onDeleteVisits(aDeletedURI) {
+ do_check_true(aDeletedURI.equals(DOWNLOAD_URI));
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+ gDownloadHistory.removeAllDownloads();
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, null, Date.now() * 1000);
+});
+
+add_test(function test_dh_addDownload_referrer()
+{
+ waitForOnVisit(function DHAD_prepareReferrer(aURI, aVisitID) {
+ do_check_true(aURI.equals(REFERRER_URI));
+ let referrerVisitId = aVisitID;
+
+ waitForOnVisit(function DHAD_onVisit(aVisitedURI, unused, unused2, unused3,
+ aReferringID) {
+ do_check_true(aVisitedURI.equals(DOWNLOAD_URI));
+ do_check_eq(aReferringID, referrerVisitId);
+
+ // Verify that the URI is already available in results at this time.
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+
+ gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
+ });
+
+ // Note that we don't pass the optional callback argument here because we must
+ // ensure that we receive the onVisit notification before we call addDownload.
+ PlacesUtils.asyncHistory.updatePlaces({
+ uri: REFERRER_URI,
+ visits: [{
+ transitionType: Ci.nsINavHistoryService.TRANSITION_TYPED,
+ visitDate: Date.now() * 1000
+ }]
+ });
+});
+
+add_test(function test_dh_addDownload_disabledHistory()
+{
+ waitForOnVisit(function DHAD_onVisit(aURI) {
+ // We should only receive the notification for the non-private URI. This
+ // test is based on the assumption that visit notifications are received in
+ // the same order of the addDownload calls, which is currently true because
+ // database access is serialized on the same worker thread.
+ do_check_true(aURI.equals(DOWNLOAD_URI));
+
+ do_check_true(!!page_in_database(DOWNLOAD_URI));
+ do_check_false(!!page_in_database(PRIVATE_URI));
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ });
+
+ Services.prefs.setBoolPref("places.history.enabled", false);
+ gDownloadHistory.addDownload(PRIVATE_URI, REFERRER_URI, Date.now() * 1000);
+
+ // The addDownload functions calls CanAddURI synchronously, thus we can set
+ // the preference back to true immediately (not all apps enable places by
+ // default).
+ Services.prefs.setBoolPref("places.history.enabled", true);
+ gDownloadHistory.addDownload(DOWNLOAD_URI, REFERRER_URI, Date.now() * 1000);
+});
+
+/**
+ * Tests that nsIDownloadHistory::AddDownload saves the additional download
+ * details if the optional destination URL is specified.
+ */
+add_test(function test_dh_details()
+{
+ const REMOTE_URI = NetUtil.newURI("http://localhost/");
+ const SOURCE_URI = NetUtil.newURI("http://example.com/test_dh_details");
+ const DEST_FILE_NAME = "dest.txt";
+
+ // We must build a real, valid file URI for the destination.
+ let destFileUri = NetUtil.newURI(FileUtils.getFile("TmpD", [DEST_FILE_NAME]));
+
+ let titleSet = false;
+ let destinationFileUriSet = false;
+ let destinationFileNameSet = false;
+
+ function checkFinished()
+ {
+ if (titleSet && destinationFileUriSet && destinationFileNameSet) {
+ PlacesUtils.annotations.removeObserver(annoObserver);
+ PlacesUtils.history.removeObserver(historyObserver);
+
+ PlacesTestUtils.clearHistory().then(run_next_test);
+ }
+ }
+
+ let annoObserver = {
+ onPageAnnotationSet: function AO_onPageAnnotationSet(aPage, aName)
+ {
+ if (aPage.equals(SOURCE_URI)) {
+ let value = PlacesUtils.annotations.getPageAnnotation(aPage, aName);
+ switch (aName)
+ {
+ case "downloads/destinationFileURI":
+ destinationFileUriSet = true;
+ do_check_eq(value, destFileUri.spec);
+ break;
+ case "downloads/destinationFileName":
+ destinationFileNameSet = true;
+ do_check_eq(value, DEST_FILE_NAME);
+ break;
+ }
+ checkFinished();
+ }
+ },
+ onItemAnnotationSet: function() {},
+ onPageAnnotationRemoved: function() {},
+ onItemAnnotationRemoved: function() {}
+ }
+
+ let historyObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function() {},
+ onTitleChanged: function HO_onTitleChanged(aURI, aPageTitle)
+ {
+ if (aURI.equals(SOURCE_URI)) {
+ titleSet = true;
+ do_check_eq(aPageTitle, DEST_FILE_NAME);
+ checkFinished();
+ }
+ },
+ onDeleteURI: function() {},
+ onClearHistory: function() {},
+ onPageChanged: function() {},
+ onDeleteVisits: function() {}
+ };
+
+ PlacesUtils.annotations.addObserver(annoObserver, false);
+ PlacesUtils.history.addObserver(historyObserver, false);
+
+ // Both null values and remote URIs should not cause errors.
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000);
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000, null);
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000, REMOTE_URI);
+
+ // Valid local file URIs should cause the download details to be saved.
+ gDownloadHistory.addDownload(SOURCE_URI, null, Date.now() * 1000,
+ destFileUri);
+});
diff --git a/toolkit/components/places/tests/unit/test_frecency.js b/toolkit/components/places/tests/unit/test_frecency.js
new file mode 100644
index 000000000..a04befe00
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency.js
@@ -0,0 +1,294 @@
+/* -*- 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 406358 to make sure frecency works for empty input/search, but
+ * this also tests for non-empty inputs as well. Because the interactions among
+ * *DIFFERENT* visit counts and visit dates is not well defined, this test
+ * holds one of the two values constant when modifying the other.
+ *
+ * Also test bug 419068 to make sure tagged pages don't necessarily have to be
+ * first in the results.
+ *
+ * Also test bug 426166 to make sure that the results of autocomplete searches
+ * are stable. Note that failures of this test will be intermittent by nature
+ * since we are testing to make sure that the unstable sort algorithm used
+ * by SQLite is not changing the order of the results on us.
+ */
+
+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: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+function ensure_results(uris, searchTerm)
+{
+ PlacesTestUtils.promiseAsyncUpdates()
+ .then(() => ensure_results_internal(uris, searchTerm));
+}
+
+function ensure_results_internal(uris, searchTerm)
+{
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ do_check_eq(controller.matchCount, uris.length);
+ for (var i=0; i<controller.matchCount; i++) {
+ do_check_eq(controller.getValueAt(i), uris[i].spec);
+ }
+
+ deferEnsureResults.resolve();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+// Get history service
+try {
+ var histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+ var bhist = histsvc.QueryInterface(Ci.nsIBrowserHistory);
+ var tagssvc = Cc["@mozilla.org/browser/tagging-service;1"].
+ getService(Ci.nsITaggingService);
+ var bmksvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+} catch (ex) {
+ do_throw("Could not get history service\n");
+}
+
+function* task_setCountDate(aURI, aCount, aDate)
+{
+ // We need visits so that frecency can be computed over multiple visits
+ let visits = [];
+ for (let i = 0; i < aCount; i++) {
+ visits.push({ uri: aURI, visitDate: aDate, transition: TRANSITION_TYPED });
+ }
+ yield PlacesTestUtils.addVisits(visits);
+}
+
+function setBookmark(aURI)
+{
+ bmksvc.insertBookmark(bmksvc.bookmarksMenuFolder, aURI, -1, "bleh");
+}
+
+function tagURI(aURI, aTags) {
+ bmksvc.insertBookmark(bmksvc.unfiledBookmarksFolder, aURI,
+ bmksvc.DEFAULT_INDEX, "bleh");
+ tagssvc.tagURI(aURI, aTags);
+}
+
+var uri1 = uri("http://site.tld/1");
+var uri2 = uri("http://site.tld/2");
+var uri3 = uri("http://aaaaaaaaaa/1");
+var uri4 = uri("http://aaaaaaaaaa/2");
+
+// d1 is younger (should show up higher) than d2 (PRTime is in usecs not msec)
+// Make sure the dates fall into different frecency buckets
+var d1 = new Date(Date.now() - 1000 * 60 * 60) * 1000;
+var d2 = new Date(Date.now() - 1000 * 60 * 60 * 24 * 10) * 1000;
+// c1 is larger (should show up higher) than c2
+var c1 = 10;
+var c2 = 1;
+
+var tests = [
+// test things without a search term
+function*() {
+ print("TEST-INFO | Test 0: same count, different date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c1, d2);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "");
+},
+function*() {
+ print("TEST-INFO | Test 1: same count, different date");
+ yield task_setCountDate(uri1, c1, d2);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "");
+},
+function*() {
+ print("TEST-INFO | Test 2: different count, same date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c2, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "");
+},
+function*() {
+ print("TEST-INFO | Test 3: different count, same date");
+ yield task_setCountDate(uri1, c2, d1);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "");
+},
+
+// test things with a search term
+function*() {
+ print("TEST-INFO | Test 4: same count, different date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c1, d2);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "site");
+},
+function*() {
+ print("TEST-INFO | Test 5: same count, different date");
+ yield task_setCountDate(uri1, c1, d2);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "site");
+},
+function*() {
+ print("TEST-INFO | Test 6: different count, same date");
+ yield task_setCountDate(uri1, c1, d1);
+ yield task_setCountDate(uri2, c2, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri1, uri2], "site");
+},
+function*() {
+ print("TEST-INFO | Test 7: different count, same date");
+ yield task_setCountDate(uri1, c2, d1);
+ yield task_setCountDate(uri2, c1, d1);
+ tagURI(uri1, ["site"]);
+ ensure_results([uri2, uri1], "site");
+},
+// There are multiple tests for 8, hence the multiple functions
+// Bug 426166 section
+function*() {
+ print("TEST-INFO | Test 8.1a: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "a");
+},
+function*() {
+ print("TEST-INFO | Test 8.1b: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aa");
+},
+function*() {
+ print("TEST-INFO | Test 8.2: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aaa");
+},
+function*() {
+ print("TEST-INFO | Test 8.3: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aaaa");
+},
+function*() {
+ print("TEST-INFO | Test 8.4: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aaa");
+},
+function*() {
+ print("TEST-INFO | Test 8.5: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "aa");
+},
+function*() {
+ print("TEST-INFO | Test 8.6: same count, same date");
+ setBookmark(uri3);
+ setBookmark(uri4);
+ ensure_results([uri4, uri3], "a");
+}
+];
+
+/**
+ * This deferred object contains a promise that is resolved when the
+ * ensure_results_internal function has finished its execution.
+ */
+var deferEnsureResults;
+
+add_task(function* test_frecency()
+{
+ // Disable autoFill for this test.
+ Services.prefs.setBoolPref("browser.urlbar.autoFill", false);
+ do_register_cleanup(() => Services.prefs.clearUserPref("browser.urlbar.autoFill"));
+ // always search in history + bookmarks, no matter what the default is
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("browser.urlbar.suggest.history", true);
+ prefs.setBoolPref("browser.urlbar.suggest.bookmark", true);
+ prefs.setBoolPref("browser.urlbar.suggest.openpage", false);
+ for (let test of tests) {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+
+ deferEnsureResults = Promise.defer();
+ yield test();
+ yield deferEnsureResults.promise;
+ }
+ for (let type of ["history", "bookmark", "openpage"]) {
+ prefs.clearUserPref("browser.urlbar.suggest." + type);
+ }
+});
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 000000000..7fadd4ae9
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_observers.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ run_next_test();
+}
+
+// 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(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 uri = NetUtil.newURI("http://example.com/a");
+ Cc["@mozilla.org/browser/download-history;1"].
+ getService(Ci.nsIDownloadHistory).
+ addDownload(uri);
+ yield Promise.all([onFrecencyChanged(uri), onFrecencyChanged(uri)]);
+});
+
+// nsNavHistory::UpdateFrecency
+add_task(function* test_nsNavHistory_UpdateFrecency() {
+ let bm = PlacesUtils.bookmarks;
+ let uri = NetUtil.newURI("http://example.com/b");
+ bm.insertBookmark(bm.unfiledBookmarksFolder, uri,
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "test");
+ yield onFrecencyChanged(uri);
+});
+
+// nsNavHistory::invalidateFrecencies for particular pages
+add_task(function* test_nsNavHistory_invalidateFrecencies_somePages() {
+ let uri = NetUtil.newURI("http://test-nsNavHistory-invalidateFrecencies-somePages.com/");
+ // Bookmarking the URI is enough to add it to moz_places, and importantly, it
+ // means that removePagesFromHost doesn't remove it from moz_places, so its
+ // frecency is able to be changed.
+ let bm = PlacesUtils.bookmarks;
+ bm.insertBookmark(bm.unfiledBookmarksFolder, uri,
+ Ci.nsINavBookmarksService.DEFAULT_INDEX, "test");
+ PlacesUtils.history.removePagesFromHost(uri.host, false);
+ yield onFrecencyChanged(uri);
+});
+
+// nsNavHistory::invalidateFrecencies for all pages
+add_task(function* test_nsNavHistory_invalidateFrecencies_allPages() {
+ yield Promise.all([onManyFrecenciesChanged(), PlacesTestUtils.clearHistory()]);
+});
+
+// nsNavHistory::DecayFrecency and nsNavHistory::FixInvalidFrecencies
+add_task(function* test_nsNavHistory_DecayFrecency_and_nsNavHistory_FixInvalidFrecencies() {
+ // FixInvalidFrecencies is at the end of a path that DecayFrecency is also on,
+ // so expect two notifications. Trigger the path by making nsNavHistory
+ // observe the idle-daily notification.
+ PlacesUtils.history.QueryInterface(Ci.nsIObserver).
+ observe(null, "idle-daily", "");
+ yield Promise.all([onManyFrecenciesChanged(), onManyFrecenciesChanged()]);
+});
+
+function onFrecencyChanged(expectedURI) {
+ let deferred = Promise.defer();
+ let obs = new NavHistoryObserver();
+ obs.onFrecencyChanged =
+ (uri, newFrecency, guid, hidden, visitDate) => {
+ PlacesUtils.history.removeObserver(obs);
+ do_check_true(!!uri);
+ do_check_true(uri.equals(expectedURI));
+ deferred.resolve();
+ };
+ PlacesUtils.history.addObserver(obs, false);
+ return deferred.promise;
+}
+
+function onManyFrecenciesChanged() {
+ let deferred = Promise.defer();
+ let obs = new NavHistoryObserver();
+ obs.onManyFrecenciesChanged = () => {
+ PlacesUtils.history.removeObserver(obs);
+ do_check_true(true);
+ deferred.resolve();
+ };
+ PlacesUtils.history.addObserver(obs, false);
+ return deferred.promise;
+}
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 000000000..e60030ca5
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_frecency_zero_updated.js
@@ -0,0 +1,30 @@
+/* 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.
+
+function run_test()
+{
+ run_next_test()
+}
+
+add_task(function* ()
+{
+ const TEST_URI = NetUtil.newURI("http://example.com/");
+ let id = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ TEST_URI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A title");
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_check_true(frecencyForUrl(TEST_URI) > 0);
+
+ // Removing the bookmark should leave an orphan page with zero frecency.
+ // Note this would usually be expired later by expiration.
+ PlacesUtils.bookmarks.removeItem(id);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ // Now add a valid visit to the page, frecency should increase.
+ yield PlacesTestUtils.addVisits({ uri: TEST_URI });
+ do_check_true(frecencyForUrl(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 000000000..4cf164d45
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_getChildIndex.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/. */
+
+/*
+ * Tests nsNavHistoryContainerResultNode::GetChildIndex(aNode) functionality.
+ */
+
+function run_test() {
+ // Add a bookmark to the menu.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri("http://test.mozilla.org/bookmark/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX,
+ "Test bookmark");
+
+ // Add a bookmark to unfiled folder.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://test.mozilla.org/unfiled/"),
+ Ci.nsINavBookmarksService.DEFAULT_INDEX,
+ "Unfiled bookmark");
+
+ // Get the unfiled bookmark node.
+ let unfiledNode = getNodeAt(PlacesUtils.unfiledBookmarksFolderId, 0);
+ if (!unfiledNode)
+ do_throw("Unable to find bookmark in hierarchy!");
+ do_check_eq(unfiledNode.title, "Unfiled bookmark");
+
+ let hs = PlacesUtils.history;
+ let query = hs.getNewQuery();
+ query.setFolders([PlacesUtils.bookmarksMenuFolderId], 1);
+ 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);
+ do_check_eq(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(aFolderId, aIndex) {
+ let hs = PlacesUtils.history;
+ let query = hs.getNewQuery();
+ query.setFolders([aFolderId], 1);
+ 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_getPlacesInfo.js b/toolkit/components/places/tests/unit/test_getPlacesInfo.js
new file mode 100644
index 000000000..3dfecb934
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_getPlacesInfo.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function promiseGetPlacesInfo(aPlacesIdentifiers) {
+ let deferred = Promise.defer();
+ PlacesUtils.asyncHistory.getPlacesInfo(aPlacesIdentifiers, {
+ _results: [],
+ _errors: [],
+
+ handleResult: function handleResult(aPlaceInfo) {
+ this._results.push(aPlaceInfo);
+ },
+ handleError: function handleError(aResultCode, aPlaceInfo) {
+ this._errors.push({ resultCode: aResultCode, info: aPlaceInfo });
+ },
+ handleCompletion: function handleCompletion() {
+ deferred.resolve({ errors: this._errors, results: this._results });
+ }
+ });
+
+ return deferred.promise;
+}
+
+function ensurePlacesInfoObjectsAreEqual(a, b) {
+ do_check_true(a.uri.equals(b.uri));
+ do_check_eq(a.title, b.title);
+ do_check_eq(a.guid, b.guid);
+ do_check_eq(a.placeId, b.placeId);
+}
+
+function* test_getPlacesInfoExistentPlace() {
+ let testURI = NetUtil.newURI("http://www.example.tld");
+ yield PlacesTestUtils.addVisits(testURI);
+
+ let getPlacesInfoResult = yield promiseGetPlacesInfo([testURI]);
+ do_check_eq(getPlacesInfoResult.results.length, 1);
+ do_check_eq(getPlacesInfoResult.errors.length, 0);
+
+ let placeInfo = getPlacesInfoResult.results[0];
+ do_check_true(placeInfo instanceof Ci.mozIPlaceInfo);
+
+ do_check_true(placeInfo.uri.equals(testURI));
+ do_check_eq(placeInfo.title, "test visit for " + testURI.spec);
+ do_check_true(placeInfo.guid.length > 0);
+ do_check_eq(placeInfo.visits, null);
+}
+add_task(test_getPlacesInfoExistentPlace);
+
+function* test_getPlacesInfoNonExistentPlace() {
+ let testURI = NetUtil.newURI("http://www.example_non_existent.tld");
+ let getPlacesInfoResult = yield promiseGetPlacesInfo(testURI);
+ do_check_eq(getPlacesInfoResult.results.length, 0);
+ do_check_eq(getPlacesInfoResult.errors.length, 1);
+}
+add_task(test_getPlacesInfoNonExistentPlace);
+
+function* test_promisedHelper() {
+ let uri = NetUtil.newURI("http://www.helper_existent_example.tld");
+ yield PlacesTestUtils.addVisits(uri);
+ let placeInfo = yield PlacesUtils.promisePlaceInfo(uri);
+ do_check_true(placeInfo instanceof Ci.mozIPlaceInfo);
+
+ uri = NetUtil.newURI("http://www.helper_non_existent_example.tld");
+ try {
+ yield PlacesUtils.promisePlaceInfo(uri);
+ do_throw("PlacesUtils.promisePlaceInfo should have rejected the promise");
+ }
+ catch (ex) { }
+}
+add_task(test_promisedHelper);
+
+function* test_infoByGUID() {
+ let testURI = NetUtil.newURI("http://www.guid_example.tld");
+ yield PlacesTestUtils.addVisits(testURI);
+
+ let placeInfoByURI = yield PlacesUtils.promisePlaceInfo(testURI);
+ let placeInfoByGUID = yield PlacesUtils.promisePlaceInfo(placeInfoByURI.guid);
+ ensurePlacesInfoObjectsAreEqual(placeInfoByURI, placeInfoByGUID);
+}
+add_task(test_infoByGUID);
+
+function* test_invalid_guid() {
+ try {
+ yield PlacesUtils.promisePlaceInfo("###");
+ do_throw("getPlacesInfo should fail for invalid guids")
+ }
+ catch (ex) { }
+}
+add_task(test_invalid_guid);
+
+function* test_mixed_selection() {
+ let placeInfo1, placeInfo2;
+ let uri = NetUtil.newURI("http://www.mixed_selection_test_1.tld");
+ yield PlacesTestUtils.addVisits(uri);
+ placeInfo1 = yield PlacesUtils.promisePlaceInfo(uri);
+
+ uri = NetUtil.newURI("http://www.mixed_selection_test_2.tld");
+ yield PlacesTestUtils.addVisits(uri);
+ placeInfo2 = yield PlacesUtils.promisePlaceInfo(uri);
+
+ let getPlacesInfoResult = yield promiseGetPlacesInfo([placeInfo1.uri, placeInfo2.guid]);
+ do_check_eq(getPlacesInfoResult.results.length, 2);
+ do_check_eq(getPlacesInfoResult.errors.length, 0);
+
+ do_check_eq(getPlacesInfoResult.results[0].uri.spec, placeInfo1.uri.spec);
+ do_check_eq(getPlacesInfoResult.results[1].guid, placeInfo2.guid);
+}
+add_task(test_mixed_selection);
+
+function run_test() {
+ run_next_test();
+}
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 000000000..8d194cde1
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history.js
@@ -0,0 +1,184 @@
+/* -*- 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
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ // we have a new profile, so we should have imported bookmarks
+ do_check_eq(histsvc.databaseStatus, histsvc.DATABASE_STATUS_CREATE);
+
+ // add a visit
+ var testURI = uri("http://mozilla.com");
+ yield 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;
+ // TODO: using full visit crashes in xpcshell test
+ // options.resultType = options.RESULTS_AS_FULL_VISIT;
+ 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
+ do_check_eq(node.uri, testURI.spec);
+ do_check_eq(node.type, Ci.nsINavHistoryResultNode.RESULT_TYPE_URI);
+ // TODO: change query type to RESULTS_AS_FULL_VISIT and test this
+ // do_check_eq(node.transitionType, histsvc.TRANSITION_TYPED);
+ }
+ root.containerOpen = false;
+
+ // add another visit for the same URI, and a third visit for a different URI
+ var testURI2 = uri("http://google.com/");
+ yield PlacesTestUtils.addVisits(testURI);
+ yield 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;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.minVisits = 1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.minVisits = 2;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ query.minVisits = 3;
+ result.root.containerOpen = false;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(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;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.maxVisits = 0;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 0);
+ result.root.containerOpen = false;
+ query.maxVisits = 1;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ result.root.containerOpen = false;
+ query.maxVisits = 2;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+ query.maxVisits = 3;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 2);
+ result.root.containerOpen = false;
+
+ // test annotation-based queries
+ var annos = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+ annos.setPageAnnotation(uri("http://mozilla.com/"), "testAnno", 0, 0,
+ Ci.nsIAnnotationService.EXPIRE_NEVER);
+ query.annotation = "testAnno";
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ do_check_eq(result.root.getChild(0).uri, "http://mozilla.com/");
+ result.root.containerOpen = false;
+
+ // test annotationIsNot
+ query.annotationIsNot = true;
+ result = histsvc.executeQuery(query, options);
+ result.root.containerOpen = true;
+ do_check_eq(result.root.childCount, 1);
+ do_check_eq(result.root.getChild(0).uri, "http://google.com/");
+ result.root.containerOpen = false;
+
+ // By default history is enabled.
+ do_check_true(!histsvc.historyDisabled);
+
+ // test getPageTitle
+ yield PlacesTestUtils.addVisits({ uri: uri("http://example.com"), title: "title" });
+ let placeInfo = yield PlacesUtils.promisePlaceInfo(uri("http://example.com"));
+ do_check_eq(placeInfo.title, "title");
+
+ // query for the visit
+ do_check_true(uri_in_db(testURI));
+
+ // test for schema changes in bug 373239
+ // get direct db connection
+ var db = histsvc.QueryInterface(Ci.nsPIPlacesDatabase).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
+ yield 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;
+ do_check_true(root.childCount > 0);
+ root.containerOpen = false;
+});
diff --git a/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js
new file mode 100644
index 000000000..a5e0e1cb1
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_autocomplete_tags.js
@@ -0,0 +1,185 @@
+/* -*- 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 current_test = 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: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// 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");
+}
+
+function ensure_tag_results(uris, searchTerm)
+{
+ print("Searching for '" + searchTerm + "'");
+ var controller = Components.classes["@mozilla.org/autocomplete/controller;1"].
+ getService(Components.interfaces.nsIAutoCompleteController);
+
+ // Make an AutoCompleteInput that uses our searches
+ // and confirms results on search complete
+ var input = new AutoCompleteInput(["unifiedcomplete"]);
+
+ controller.input = input;
+
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function() {
+ do_check_eq(numSearchesStarted, 1);
+ do_check_eq(controller.searchStatus,
+ uris.length ?
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH :
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+ do_check_eq(controller.matchCount, uris.length);
+ let vals = [];
+ for (let i=0; i<controller.matchCount; i++) {
+ // Keep the URL for later because order of tag results is undefined
+ vals.push(controller.getValueAt(i));
+ do_check_eq(controller.getStyleAt(i), "bookmark-tag");
+ }
+ // Sort the results then check if we have the right items
+ vals.sort().forEach((val, i) => do_check_eq(val, uris[i].spec))
+
+ if (current_test < (tests.length - 1)) {
+ current_test++;
+ tests[current_test]();
+ }
+
+ do_test_finished();
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+var uri1 = uri("http://site.tld/1/aaa");
+var uri2 = uri("http://site.tld/2/bbb");
+var uri3 = uri("http://site.tld/3/aaa");
+var uri4 = uri("http://site.tld/4/bbb");
+var uri5 = uri("http://site.tld/5/aaa");
+var uri6 = uri("http://site.tld/6/bbb");
+
+var tests = [
+ () => ensure_tag_results([uri1, uri4, uri6], "foo"),
+ () => ensure_tag_results([uri1], "foo aaa"),
+ () => ensure_tag_results([uri4, uri6], "foo bbb"),
+ () => ensure_tag_results([uri2, uri4, uri5, uri6], "bar"),
+ () => ensure_tag_results([uri5], "bar aaa"),
+ () => ensure_tag_results([uri2, uri4, uri6], "bar bbb"),
+ () => ensure_tag_results([uri3, uri5, uri6], "cheese"),
+ () => ensure_tag_results([uri3, uri5], "chees aaa"),
+ () => ensure_tag_results([uri6], "chees bbb"),
+ () => ensure_tag_results([uri4, uri6], "fo bar"),
+ () => ensure_tag_results([], "fo bar aaa"),
+ () => ensure_tag_results([uri4, uri6], "fo bar bbb"),
+ () => ensure_tag_results([uri4, uri6], "ba foo"),
+ () => ensure_tag_results([], "ba foo aaa"),
+ () => ensure_tag_results([uri4, uri6], "ba foo bbb"),
+ () => ensure_tag_results([uri5, uri6], "ba chee"),
+ () => ensure_tag_results([uri5], "ba chee aaa"),
+ () => ensure_tag_results([uri6], "ba chee bbb"),
+ () => ensure_tag_results([uri5, uri6], "cheese bar"),
+ () => ensure_tag_results([uri5], "cheese bar aaa"),
+ () => ensure_tag_results([uri6], "chees bar bbb"),
+ () => ensure_tag_results([uri6], "cheese bar foo"),
+ () => ensure_tag_results([], "foo bar cheese aaa"),
+ () => ensure_tag_results([uri6], "foo bar cheese bbb"),
+];
+
+/**
+ * Properly tags a uri adding it to bookmarks.
+ *
+ * @param aURI
+ * The nsIURI to tag.
+ * @param aTags
+ * The tags to add.
+ */
+function tagURI(aURI, aTags) {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ aURI,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "A title");
+ tagssvc.tagURI(aURI, aTags);
+}
+
+/**
+ * Test history autocomplete
+ */
+function run_test() {
+ // always search in history + bookmarks, no matter what the default is
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setIntPref("browser.urlbar.search.sources", 3);
+ prefs.setIntPref("browser.urlbar.default.behavior", 0);
+
+ tagURI(uri1, ["foo"]);
+ tagURI(uri2, ["bar"]);
+ tagURI(uri3, ["cheese"]);
+ tagURI(uri4, ["foo bar"]);
+ tagURI(uri5, ["bar cheese"]);
+ tagURI(uri6, ["foo bar cheese"]);
+
+ tests[0]();
+}
diff --git a/toolkit/components/places/tests/unit/test_history_catobs.js b/toolkit/components/places/tests/unit/test_history_catobs.js
new file mode 100644
index 000000000..e0a81d67b
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_catobs.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ do_load_manifest("nsDummyObserver.manifest");
+
+ let dummyCreated = false;
+ let dummyReceivedOnVisit = false;
+
+ Services.obs.addObserver(function created() {
+ Services.obs.removeObserver(created, "dummy-observer-created");
+ dummyCreated = true;
+ }, "dummy-observer-created", false);
+ Services.obs.addObserver(function visited() {
+ Services.obs.removeObserver(visited, "dummy-observer-visited");
+ dummyReceivedOnVisit = true;
+ }, "dummy-observer-visited", false);
+
+ let initialObservers = PlacesUtils.history.getObservers();
+
+ // Add a common observer, it should be invoked after the category observer.
+ let notificationsPromised = new Promise((resolve, reject) => {
+ PlacesUtils.history.addObserver({
+ __proto__: NavHistoryObserver.prototype,
+ onVisit() {
+ let observers = PlacesUtils.history.getObservers();
+ Assert.equal(observers.length, initialObservers.length + 1);
+
+ // Check the common observer is the last one.
+ for (let i = 0; i < initialObservers.length; ++i) {
+ Assert.equal(initialObservers[i], observers[i]);
+ }
+
+ PlacesUtils.history.removeObserver(this);
+ observers = PlacesUtils.history.getObservers();
+ Assert.equal(observers.length, initialObservers.length);
+
+ // Check the category observer has been invoked before this one.
+ Assert.ok(dummyCreated);
+ Assert.ok(dummyReceivedOnVisit);
+ resolve();
+ }
+ }, false);
+ });
+
+ // Add a visit.
+ yield PlacesTestUtils.addVisits(uri("http://typed.mozilla.org"));
+
+ yield notificationsPromised;
+});
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 000000000..56d34994f
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_clear.js
@@ -0,0 +1,169 @@
+/* -*- 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();
+
+function promiseOnClearHistoryObserved() {
+ let deferred = Promise.defer();
+
+ let historyObserver = {
+ onBeginUpdateBatch: function() {},
+ onEndUpdateBatch: function() {},
+ onVisit: function() {},
+ onTitleChanged: function() {},
+ onDeleteURI: function(aURI) {},
+ onPageChanged: function() {},
+ onDeleteVisits: function() {},
+
+ onClearHistory: function() {
+ PlacesUtils.history.removeObserver(this, false);
+ deferred.resolve();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavHistoryObserver,
+ ])
+ }
+ PlacesUtils.history.addObserver(historyObserver, false);
+ return deferred.promise;
+}
+
+// This global variable is a promise object, initialized in run_test and waited
+// upon in the first asynchronous test. It is resolved when the
+// "places-init-complete" notification is received. We cannot initialize it in
+// the asynchronous test, because then it's too late to register the observer.
+var promiseInit;
+
+function run_test() {
+ // places-init-complete is notified after run_test, and it will
+ // run a first frecency fix through async statements.
+ // To avoid random failures we have to run after all of this.
+ promiseInit = promiseTopicObserved(PlacesUtils.TOPIC_INIT_COMPLETE);
+
+ run_next_test();
+}
+
+add_task(function* test_history_clear()
+{
+ yield promiseInit;
+
+ yield 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
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("place:folder=4"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "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.
+ PlacesUtils.annotations.setPageAnnotation(uri("http://download.mozilla.org/"),
+ "never", "never", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Add a bookmark
+ // Bookmarked page should have history cleared and frecency = -1
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ uri("http://typed.mozilla.org/"),
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: uri("http://typed.mozilla.org/"),
+ transition: TRANSITION_BOOKMARK },
+ { uri: uri("http://frecency.mozilla.org/"),
+ transition: TRANSITION_LINK },
+ ]);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // Clear history and wait for the onClearHistory notification.
+ let promiseWaitClearHistory = promiseOnClearHistoryObserved();
+ PlacesUtils.history.clear();
+ yield promiseWaitClearHistory;
+
+ // check browserHistory returns no entries
+ do_check_eq(0, PlacesUtils.history.hasHistoryEntries);
+
+ yield promiseTopicObserved(PlacesUtils.TOPIC_EXPIRATION_FINISHED);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // Check that frecency for not cleared items (bookmarks) has been converted
+ // to -1.
+ stmt = mDBConn.createStatement(
+ "SELECT h.id FROM moz_places h WHERE h.frecency > 0 ");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h WHERE h.frecency < 0
+ AND EXISTS (SELECT id FROM moz_bookmarks WHERE fk = h.id) LIMIT 1`);
+ do_check_true(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");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that history tables are empty
+ stmt = mDBConn.createStatement(
+ "SELECT * FROM (SELECT id FROM moz_historyvisits LIMIT 1)");
+ do_check_false(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`);
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+
+ // Check that we only have favicons for retained places
+ stmt = mDBConn.createStatement(
+ `SELECT f.id FROM moz_favicons f WHERE NOT EXISTS
+ (SELECT id FROM moz_places WHERE favicon_id = f.id) LIMIT 1`);
+ do_check_false(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`);
+ do_check_false(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`);
+ do_check_false(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`);
+ do_check_false(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 000000000..4e1e635a0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_notifications.js
@@ -0,0 +1,38 @@
+const NS_PLACES_INIT_COMPLETE_TOPIC = "places-init-complete";
+const NS_PLACES_DATABASE_LOCKED_TOPIC = "places-database-locked";
+
+add_task(function* () {
+ // Create a dummy places.sqlite and open an unshared connection on it
+ let db = Services.dirsvc.get('ProfD', Ci.nsIFile);
+ db.append("places.sqlite");
+ let dbConn = Services.storage.openUnsharedDatabase(db);
+ Assert.ok(db.exists(), "The database should have been created");
+
+ // We need an exclusive lock on the db
+ dbConn.executeSimpleSQL("PRAGMA locking_mode = EXCLUSIVE");
+ // Exclusive locking is lazy applied, we need to make a write to activate it
+ dbConn.executeSimpleSQL("PRAGMA USER_VERSION = 1");
+
+ // Try to create history service while the db is locked
+ let promiseLocked = promiseTopicObserved(NS_PLACES_DATABASE_LOCKED_TOPIC);
+ Assert.throws(() => Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService),
+ /NS_ERROR_XPC_GS_RETURNED_FAILURE/);
+ yield promiseLocked;
+
+ // Close our connection and try to cleanup the file (could fail on Windows)
+ dbConn.close();
+ if (db.exists()) {
+ try {
+ db.remove(false);
+ } catch (e) {
+ do_print("Unable to remove dummy places.sqlite");
+ }
+ }
+
+ // Create history service correctly
+ let promiseComplete = promiseTopicObserved(NS_PLACES_INIT_COMPLETE_TOPIC);
+ Cc["@mozilla.org/browser/nav-history-service;1"]
+ .getService(Ci.nsINavHistoryService);
+ yield promiseComplete;
+});
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 000000000..c101cfb61
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_observer.js
@@ -0,0 +1,215 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Generic nsINavHistoryObserver that doesn't implement anything, but provides
+ * dummy methods to prevent errors about an object not having a certain method.
+ */
+function NavHistoryObserver() {
+}
+NavHistoryObserver.prototype = {
+ onBeginUpdateBatch: function() { },
+ onEndUpdateBatch: function() { },
+ onVisit: function() { },
+ onTitleChanged: function() { },
+ onDeleteURI: function() { },
+ onClearHistory: function() { },
+ onPageChanged: function() { },
+ onDeleteVisits: function() { },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavHistoryObserver])
+};
+
+/**
+ * Registers a one-time history observer for and calls the callback
+ * when the specified nsINavHistoryObserver method is called.
+ * Returns a promise that is resolved when the callback returns.
+ */
+function onNotify(callback) {
+ return new Promise(resolve => {
+ let obs = new NavHistoryObserver();
+ obs[callback.name] = function () {
+ PlacesUtils.history.removeObserver(this);
+ callback.apply(this, arguments);
+ resolve();
+ };
+ PlacesUtils.history.addObserver(obs, false);
+ });
+}
+
+/**
+ * Asynchronous task that adds a visit to the history database.
+ */
+function* task_add_visit(uri, timestamp, transition) {
+ uri = uri || NetUtil.newURI("http://firefox.com/");
+ timestamp = timestamp || Date.now() * 1000;
+ yield PlacesTestUtils.addVisits({
+ uri: uri,
+ transition: transition || TRANSITION_TYPED,
+ visitDate: timestamp
+ });
+ return [uri, timestamp];
+}
+
+add_task(function* test_onVisit() {
+ let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
+ aSessionID, aReferringID,
+ aTransitionType, aGUID,
+ aHidden, aVisitCount, aTyped) {
+ Assert.ok(aURI.equals(testuri));
+ Assert.ok(aVisitID > 0);
+ Assert.equal(aTime, testtime);
+ Assert.equal(aSessionID, 0);
+ Assert.equal(aReferringID, 0);
+ Assert.equal(aTransitionType, TRANSITION_TYPED);
+ do_check_guid_for_uri(aURI, aGUID);
+ Assert.ok(!aHidden);
+ Assert.equal(aVisitCount, 1);
+ Assert.equal(aTyped, 1);
+ });
+ let testuri = NetUtil.newURI("http://firefox.com/");
+ let testtime = Date.now() * 1000;
+ yield task_add_visit(testuri, testtime);
+ yield promiseNotify;
+});
+
+add_task(function* test_onVisit() {
+ let promiseNotify = onNotify(function onVisit(aURI, aVisitID, aTime,
+ aSessionID, aReferringID,
+ aTransitionType, aGUID,
+ aHidden, aVisitCount, aTyped) {
+ Assert.ok(aURI.equals(testuri));
+ Assert.ok(aVisitID > 0);
+ Assert.equal(aTime, testtime);
+ Assert.equal(aSessionID, 0);
+ Assert.equal(aReferringID, 0);
+ Assert.equal(aTransitionType, TRANSITION_FRAMED_LINK);
+ do_check_guid_for_uri(aURI, aGUID);
+ Assert.ok(aHidden);
+ Assert.equal(aVisitCount, 1);
+ Assert.equal(aTyped, 0);
+ });
+ let testuri = NetUtil.newURI("http://hidden.firefox.com/");
+ let testtime = Date.now() * 1000;
+ yield task_add_visit(testuri, testtime, TRANSITION_FRAMED_LINK);
+ yield promiseNotify;
+});
+
+add_task(function* test_multiple_onVisit() {
+ let testuri = NetUtil.newURI("http://self.firefox.com/");
+ let promiseNotifications = new Promise(resolve => {
+ let observer = {
+ _c: 0,
+ __proto__: NavHistoryObserver.prototype,
+ onVisit(uri, id, time, unused, referrerId, transition, guid,
+ hidden, visitCount, typed) {
+ Assert.ok(testuri.equals(uri));
+ Assert.ok(id > 0);
+ Assert.ok(time > 0);
+ Assert.ok(!hidden);
+ do_check_guid_for_uri(uri, guid);
+ switch (++this._c) {
+ case 1:
+ Assert.equal(referrerId, 0);
+ Assert.equal(transition, TRANSITION_LINK);
+ Assert.equal(visitCount, 1);
+ Assert.equal(typed, 0);
+ break;
+ case 2:
+ Assert.ok(referrerId > 0);
+ Assert.equal(transition, TRANSITION_LINK);
+ Assert.equal(visitCount, 2);
+ Assert.equal(typed, 0);
+ break;
+ case 3:
+ Assert.equal(referrerId, 0);
+ Assert.equal(transition, TRANSITION_TYPED);
+ Assert.equal(visitCount, 3);
+ Assert.equal(typed, 1);
+
+ PlacesUtils.history.removeObserver(observer, false);
+ resolve();
+ break;
+ }
+ }
+ };
+ PlacesUtils.history.addObserver(observer, false);
+ });
+ yield PlacesTestUtils.addVisits([
+ { uri: testuri, transition: TRANSITION_LINK },
+ { uri: testuri, referrer: testuri, transition: TRANSITION_LINK },
+ { uri: testuri, transition: TRANSITION_TYPED },
+ ]);
+ yield promiseNotifications;
+});
+
+add_task(function* test_onDeleteURI() {
+ let promiseNotify = onNotify(function onDeleteURI(aURI, aGUID, aReason) {
+ Assert.ok(aURI.equals(testuri));
+ // Can't use do_check_guid_for_uri() here because the visit is already gone.
+ Assert.equal(aGUID, testguid);
+ Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
+ });
+ let [testuri] = yield task_add_visit();
+ let testguid = do_get_guid_for_uri(testuri);
+ PlacesUtils.bhistory.removePage(testuri);
+ yield promiseNotify;
+});
+
+add_task(function* test_onDeleteVisits() {
+ let promiseNotify = onNotify(function onDeleteVisits(aURI, aVisitTime, aGUID,
+ aReason) {
+ Assert.ok(aURI.equals(testuri));
+ // Can't use do_check_guid_for_uri() here because the visit is already gone.
+ Assert.equal(aGUID, testguid);
+ Assert.equal(aReason, Ci.nsINavHistoryObserver.REASON_DELETED);
+ Assert.equal(aVisitTime, 0); // All visits have been removed.
+ });
+ let msecs24hrsAgo = Date.now() - (86400 * 1000);
+ let [testuri] = yield task_add_visit(undefined, msecs24hrsAgo * 1000);
+ // Add a bookmark so the page is not removed.
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ testuri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "test");
+ let testguid = do_get_guid_for_uri(testuri);
+ PlacesUtils.bhistory.removePage(testuri);
+ yield promiseNotify;
+});
+
+add_task(function* test_onTitleChanged() {
+ let promiseNotify = onNotify(function onTitleChanged(aURI, aTitle, aGUID) {
+ Assert.ok(aURI.equals(testuri));
+ Assert.equal(aTitle, title);
+ do_check_guid_for_uri(aURI, aGUID);
+ });
+
+ let [testuri] = yield task_add_visit();
+ let title = "test-title";
+ yield PlacesTestUtils.addVisits({
+ uri: testuri,
+ title: title
+ });
+ yield promiseNotify;
+});
+
+add_task(function* test_onPageChanged() {
+ let promiseNotify = onNotify(function onPageChanged(aURI, aChangedAttribute,
+ aNewValue, aGUID) {
+ Assert.equal(aChangedAttribute, Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON);
+ Assert.ok(aURI.equals(testuri));
+ Assert.equal(aNewValue, SMALLPNG_DATA_URI.spec);
+ do_check_guid_for_uri(aURI, aGUID);
+ });
+
+ let [testuri] = yield task_add_visit();
+
+ // The new favicon for the page must have data associated with it in order to
+ // receive the onPageChanged notification. To keep this test self-contained,
+ // we use an URI representing the smallest possible PNG file.
+ PlacesUtils.favicons.setAndFetchFaviconForPage(testuri, SMALLPNG_DATA_URI,
+ false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ yield promiseNotify;
+});
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 000000000..1c03547d7
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_history_sidebar.js
@@ -0,0 +1,447 @@
+/* -*- 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
+var hs = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+var bh = hs.QueryInterface(Ci.nsIBrowserHistory);
+var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+var ps = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+/**
+ * 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.
+ */
+function* task_add_normalized_visit(aURI, aTime, aDayOffset) {
+ var dateObj = new Date(aTime);
+ // Normalize to midnight
+ dateObj.setHours(0);
+ dateObj.setMinutes(0);
+ dateObj.setSeconds(0);
+ dateObj.setMilliseconds(0);
+ // Days where DST changes should be taken in count.
+ var previousDateObj = new Date(dateObj.getTime() + aDayOffset * 86400000);
+ var DSTCorrection = (dateObj.getTimezoneOffset() -
+ previousDateObj.getTimezoneOffset()) * 60 * 1000;
+ // Substract aDayOffset
+ var PRTimeWithOffset = (previousDateObj.getTime() - DSTCorrection) * 1000;
+ var timeInMs = new Date(PRTimeWithOffset/1000);
+ print("Adding visit to " + aURI.spec + " at " + timeInMs);
+ yield PlacesTestUtils.addVisits({
+ uri: aURI,
+ visitDate: PRTimeWithOffset
+ });
+}
+
+function days_for_x_months_ago(aNowObj, aMonths) {
+ var oldTime = new Date();
+ // Set day before month, otherwise we could try to calculate 30 February, or
+ // other nonexistent days.
+ oldTime.setDate(1);
+ oldTime.setMonth(aNowObj.getMonth() - aMonths);
+ oldTime.setHours(0);
+ oldTime.setMinutes(0);
+ oldTime.setSeconds(0);
+ // Stay larger for eventual timezone issues, add 2 days.
+ return parseInt((aNowObj - oldTime) / (1000*60*60*24)) + 2;
+}
+
+var nowObj = new Date();
+// This test relies on en-US locale
+// Offset is number of days
+/* eslint-disable comma-spacing */
+var containers = [
+ { label: "Today" , offset: 0 , visible: true },
+ { label: "Yesterday" , offset: -1 , visible: true },
+ { label: "Last 7 days" , offset: -3 , visible: true },
+ { label: "This month" , offset: -8 , visible: nowObj.getDate() > 8 },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 0) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 1) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 2) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 3) , visible: true },
+ { label: "" , offset: -days_for_x_months_ago(nowObj, 4) , visible: true },
+ { label: "Older than 6 months" , offset: -days_for_x_months_ago(nowObj, 5) , visible: true },
+];
+/* eslint-enable comma-spacing */
+
+var visibleContainers = containers.filter(
+ function(aContainer) { return aContainer.visible });
+
+/**
+ * Asynchronous task that fills history and checks containers' labels.
+ */
+function* task_fill_history() {
+ print("\n\n*** TEST Fill History\n");
+ // 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];
+ var testURI = uri("http://mirror"+i+".mozilla.com/b");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset);
+ testURI = uri("http://mirror"+i+".mozilla.com/a");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset);
+ testURI = uri("http://mirror"+i+".google.com/b");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), container.offset);
+ testURI = uri("http://mirror"+i+".google.com/a");
+ yield task_add_normalized_visit(testURI, nowObj.getTime(), 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);
+ }
+
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ var query = hs.getNewQuery();
+
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ var cc = root.childCount;
+ print("Found containers:");
+ var previousLabels = [];
+ for (let i = 0; i < cc; i++) {
+ let container = visibleContainers[i];
+ var node = root.getChild(i);
+ print(node.title);
+ if (container.label)
+ do_check_eq(node.title, container.label);
+ // Check labels are not repeated.
+ do_check_eq(previousLabels.indexOf(node.title), -1);
+ previousLabels.push(node.title);
+ }
+ do_check_eq(cc, visibleContainers.length);
+ root.containerOpen = false;
+}
+
+/**
+ * Bug 485703 - Hide date containers not containing additional entries compared
+ * to previous ones.
+ */
+function check_visit(aOffset) {
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ var cc = root.childCount;
+
+ var unexpected = [];
+ switch (aOffset) {
+ case 0:
+ unexpected = ["Yesterday", "Last 7 days", "This month"];
+ break;
+ case -1:
+ unexpected = ["Last 7 days", "This month"];
+ break;
+ case -3:
+ unexpected = ["This month"];
+ break;
+ default:
+ // Other containers are tested later.
+ }
+
+ print("Found containers:");
+ for (var i = 0; i < cc; i++) {
+ var node = root.getChild(i);
+ print(node.title);
+ do_check_eq(unexpected.indexOf(node.title), -1);
+ }
+
+ root.containerOpen = false;
+}
+
+/**
+ * Queries history grouped by date and site, checking containers' labels and
+ * children.
+ */
+function test_RESULTS_AS_DATE_SITE_QUERY() {
+ print("\n\n*** TEST RESULTS_AS_DATE_SITE_QUERY\n");
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_SITE_QUERY;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ // Check one of the days
+ var dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 2);
+
+ // Items should be sorted by host
+ var site1 = dayNode.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site1.title, "mirror0.google.com");
+
+ var site2 = dayNode.getChild(1)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site2.title, "mirror0.mozilla.com");
+
+ site1.containerOpen = true;
+ do_check_eq(site1.childCount, 2);
+
+ // Inside of host sites are sorted by title
+ var site1visit = site1.getChild(0);
+ do_check_eq(site1visit.uri, "http://mirror0.google.com/a");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = options.SORT_BY_TITLE_DESCENDING;
+
+ // Check one of the days
+ dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 2);
+
+ // Hosts are still sorted by title
+ site1 = dayNode.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site1.title, "mirror0.google.com");
+
+ site2 = dayNode.getChild(1)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(site2.title, "mirror0.mozilla.com");
+
+ site1.containerOpen = true;
+ do_check_eq(site1.childCount, 2);
+
+ // But URLs are now sorted by title descending
+ site1visit = site1.getChild(0);
+ do_check_eq(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.
+ */
+function test_RESULTS_AS_DATE_QUERY() {
+ print("\n\n*** TEST RESULTS_AS_DATE_QUERY\n");
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_DATE_QUERY;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ var cc = root.childCount;
+ do_check_eq(cc, visibleContainers.length);
+ print("Found containers:");
+ for (var i = 0; i < cc; i++) {
+ var container = visibleContainers[i];
+ var node = root.getChild(i);
+ print(node.title);
+ if (container.label)
+ do_check_eq(node.title, container.label);
+ }
+
+ // Check one of the days
+ var dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 4);
+
+ // Items should be sorted by title
+ var visit1 = dayNode.getChild(0);
+ do_check_eq(visit1.uri, "http://mirror0.google.com/a");
+
+ var visit2 = dayNode.getChild(3);
+ do_check_eq(visit2.uri, "http://mirror0.mozilla.com/b");
+
+ // Bug 473157: changing sorting mode should not affect the containers
+ result.sortingMode = options.SORT_BY_TITLE_DESCENDING;
+
+ // Check one of the days
+ dayNode = root.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dayNode.containerOpen = true;
+ do_check_eq(dayNode.childCount, 4);
+
+ // But URLs are now sorted by title descending
+ visit1 = dayNode.getChild(0);
+ do_check_eq(visit1.uri, "http://mirror0.mozilla.com/b");
+
+ visit2 = dayNode.getChild(3);
+ do_check_eq(visit2.uri, "http://mirror0.google.com/a");
+
+ dayNode.containerOpen = false;
+ root.containerOpen = false;
+}
+
+/**
+ * Queries history grouped by site, checking containers' labels and children.
+ */
+function test_RESULTS_AS_SITE_QUERY() {
+ print("\n\n*** TEST RESULTS_AS_SITE_QUERY\n");
+ // add a bookmark with a domain not in the set of visits in the db
+ var itemId = bs.insertBookmark(bs.toolbarFolder, uri("http://foobar"),
+ bs.DEFAULT_INDEX, "");
+
+ var options = hs.getNewQueryOptions();
+ options.resultType = options.RESULTS_AS_SITE_QUERY;
+ options.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+ do_check_eq(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
+ var siteNode = root.getChild(6)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(siteNode.title, "mirror3.google.com");
+
+ siteNode.containerOpen = true;
+ do_check_eq(siteNode.childCount, 2);
+
+ // Inside of host sites are sorted by title
+ var visitNode = siteNode.getChild(0);
+ do_check_eq(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 = root.getChild(6)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ do_check_eq(siteNode.title, "mirror3.google.com");
+
+ siteNode.containerOpen = true;
+ do_check_eq(siteNode.childCount, 2);
+
+ // But URLs are now sorted by title descending
+ var visit = siteNode.getChild(0);
+ do_check_eq(visit.uri, "http://mirror3.google.com/b");
+
+ siteNode.containerOpen = false;
+ root.containerOpen = false;
+
+ // Cleanup.
+ bs.removeItem(itemId);
+}
+
+/**
+ * Checks that queries grouped by date do liveupdate correctly.
+ */
+function* task_test_date_liveupdate(aResultType) {
+ var midnight = nowObj;
+ midnight.setHours(0);
+ midnight.setMinutes(0);
+ midnight.setSeconds(0);
+ midnight.setMilliseconds(0);
+
+ // TEST 1. Test that the query correctly updates when it is root.
+ var options = hs.getNewQueryOptions();
+ options.resultType = aResultType;
+ var query = hs.getNewQuery();
+ var result = hs.executeQuery(query, options);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_eq(root.childCount, visibleContainers.length);
+ // Remove "Today".
+ hs.removePagesByTimeframe(midnight.getTime() * 1000, Date.now() * 1000);
+ do_check_eq(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.
+ var last7Days = root.getChild(1)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ last7Days.containerOpen = true;
+
+ // Add a visit for "Today". This should add back the missing "Today"
+ // container.
+ yield task_add_normalized_visit(uri("http://www.mozilla.org/"), nowObj.getTime(), 0);
+ do_check_eq(root.childCount, visibleContainers.length);
+
+ last7Days.containerOpen = false;
+ root.containerOpen = false;
+
+ // TEST 2. Test that the query correctly updates even if it is not root.
+ var itemId = bs.insertBookmark(bs.toolbarFolder,
+ uri("place:type=" + aResultType),
+ bs.DEFAULT_INDEX, "");
+
+ // Query toolbar and open our query container, then check again liveupdate.
+ options = hs.getNewQueryOptions();
+ query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ result = hs.executeQuery(query, options);
+ root = result.root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+ var dateContainer = root.getChild(0).QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ dateContainer.containerOpen = true;
+
+ do_check_eq(dateContainer.childCount, visibleContainers.length);
+ // Remove "Today".
+ hs.removePagesByTimeframe(midnight.getTime() * 1000, Date.now() * 1000);
+ do_check_eq(dateContainer.childCount, visibleContainers.length - 1);
+ // Add a visit for "Today".
+ yield task_add_normalized_visit(uri("http://www.mozilla.org/"), nowObj.getTime(), 0);
+ do_check_eq(dateContainer.childCount, visibleContainers.length);
+
+ dateContainer.containerOpen = false;
+ root.containerOpen = false;
+
+ // Cleanup.
+ bs.removeItem(itemId);
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_history_sidebar()
+{
+ // If we're dangerously close to a date change, just bail out.
+ if (nowObj.getHours() == 23 && nowObj.getMinutes() >= 50) {
+ return;
+ }
+
+ yield task_fill_history();
+ test_RESULTS_AS_DATE_SITE_QUERY();
+ test_RESULTS_AS_DATE_QUERY();
+ test_RESULTS_AS_SITE_QUERY();
+
+ yield task_test_date_liveupdate(Ci.nsINavHistoryQueryOptions.RESULTS_AS_DATE_SITE_QUERY);
+ yield task_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_hosts_triggers.js b/toolkit/components/places/tests/unit/test_hosts_triggers.js
new file mode 100644
index 000000000..9c3359e76
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_hosts_triggers.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * This file tests the validity of various triggers that add remove hosts from moz_hosts
+ */
+
+XPCOMUtils.defineLazyServiceGetter(this, "gHistory",
+ "@mozilla.org/browser/history;1",
+ "mozIAsyncHistory");
+
+// add some visits and remove them, add a bookmark,
+// change its uri, then remove it, and
+// for each change check that moz_hosts has correctly been updated.
+
+function isHostInMozPlaces(aURI)
+{
+ let stmt = DBConn().createStatement(
+ `SELECT url
+ FROM moz_places
+ WHERE url_hash = hash(:host) AND url = :host`
+ );
+ let result = false;
+ stmt.params.host = aURI.spec;
+ while (stmt.executeStep()) {
+ if (stmt.row.url == aURI.spec) {
+ result = true;
+ break;
+ }
+ }
+ stmt.finalize();
+ return result;
+}
+
+function isHostInMozHosts(aURI, aTyped, aPrefix)
+{
+ let stmt = DBConn().createStatement(
+ `SELECT host, typed, prefix
+ FROM moz_hosts
+ WHERE host = fixup_url(:host)
+ AND frecency NOTNULL`
+ );
+ let result = false;
+ stmt.params.host = aURI.host;
+ if (stmt.executeStep()) {
+ result = aTyped == stmt.row.typed && aPrefix == stmt.row.prefix;
+ }
+ stmt.finalize();
+ return result;
+}
+
+var urls = [{uri: NetUtil.newURI("http://visit1.mozilla.org"),
+ expected: "visit1.mozilla.org",
+ typed: 0,
+ prefix: null
+ },
+ {uri: NetUtil.newURI("http://visit2.mozilla.org"),
+ expected: "visit2.mozilla.org",
+ typed: 0,
+ prefix: null
+ },
+ {uri: NetUtil.newURI("http://www.foo.mozilla.org"),
+ expected: "foo.mozilla.org",
+ typed: 1,
+ prefix: "www."
+ },
+ ];
+
+const NEW_URL = "http://different.mozilla.org/";
+
+add_task(function* test_moz_hosts_update()
+{
+ let places = [];
+ urls.forEach(function(url) {
+ let place = { uri: url.uri,
+ title: "test for " + url.url,
+ transition: url.typed ? TRANSITION_TYPED : undefined };
+ places.push(place);
+ });
+
+ yield PlacesTestUtils.addVisits(places);
+
+ do_check_true(isHostInMozHosts(urls[0].uri, urls[0].typed, urls[0].prefix));
+ do_check_true(isHostInMozHosts(urls[1].uri, urls[1].typed, urls[1].prefix));
+ do_check_true(isHostInMozHosts(urls[2].uri, urls[2].typed, urls[2].prefix));
+});
+
+add_task(function* test_remove_places()
+{
+ for (let idx in urls) {
+ PlacesUtils.history.removePage(urls[idx].uri);
+ }
+
+ yield PlacesTestUtils.clearHistory();
+
+ for (let idx in urls) {
+ do_check_false(isHostInMozHosts(urls[idx].uri, urls[idx].typed, urls[idx].prefix));
+ }
+});
+
+add_task(function* test_bookmark_changes()
+{
+ let testUri = NetUtil.newURI("http://test.mozilla.org");
+
+ let itemId = PlacesUtils.bookmarks.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ testUri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "bookmark title");
+
+ do_check_true(isHostInMozPlaces(testUri));
+
+ // Change the hostname
+ PlacesUtils.bookmarks.changeBookmarkURI(itemId, NetUtil.newURI(NEW_URL));
+
+ yield PlacesTestUtils.clearHistory();
+
+ let newUri = NetUtil.newURI(NEW_URL);
+ do_check_true(isHostInMozPlaces(newUri));
+ do_check_true(isHostInMozHosts(newUri, false, null));
+ do_check_false(isHostInMozHosts(NetUtil.newURI("http://test.mozilla.org"), false, null));
+});
+
+add_task(function* test_bookmark_removal()
+{
+ let itemId = PlacesUtils.bookmarks.getIdForItemAt(PlacesUtils.unfiledBookmarksFolderId,
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let newUri = NetUtil.newURI(NEW_URL);
+ PlacesUtils.bookmarks.removeItem(itemId);
+ yield PlacesTestUtils.clearHistory();
+
+ do_check_false(isHostInMozHosts(newUri, false, null));
+});
+
+add_task(function* test_moz_hosts_typed_update()
+{
+ const TEST_URI = NetUtil.newURI("http://typed.mozilla.com");
+ let places = [{ uri: TEST_URI
+ , title: "test for " + TEST_URI.spec
+ },
+ { uri: TEST_URI
+ , title: "test for " + TEST_URI.spec
+ , transition: TRANSITION_TYPED
+ }];
+
+ yield PlacesTestUtils.addVisits(places);
+
+ do_check_true(isHostInMozHosts(TEST_URI, true, null));
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_moz_hosts_www_remove()
+{
+ function* test_removal(aURIToRemove, aURIToKeep, aCallback) {
+ let places = [{ uri: aURIToRemove
+ , title: "test for " + aURIToRemove.spec
+ , transition: TRANSITION_TYPED
+ },
+ { uri: aURIToKeep
+ , title: "test for " + aURIToKeep.spec
+ , transition: TRANSITION_TYPED
+ }];
+
+ yield PlacesTestUtils.addVisits(places);
+ print("removing " + aURIToRemove.spec + " keeping " + aURIToKeep);
+ dump_table("moz_hosts");
+ dump_table("moz_places");
+ PlacesUtils.history.removePage(aURIToRemove);
+ let prefix = /www/.test(aURIToKeep.spec) ? "www." : null;
+ dump_table("moz_hosts");
+ dump_table("moz_places");
+ do_check_true(isHostInMozHosts(aURIToKeep, true, prefix));
+ }
+
+ const TEST_URI = NetUtil.newURI("http://rem.mozilla.com");
+ const TEST_WWW_URI = NetUtil.newURI("http://www.rem.mozilla.com");
+ yield test_removal(TEST_URI, TEST_WWW_URI);
+ yield test_removal(TEST_WWW_URI, TEST_URI);
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_moz_hosts_ftp_matchall()
+{
+ const TEST_URI_1 = NetUtil.newURI("ftp://www.mozilla.com/");
+ const TEST_URI_2 = NetUtil.newURI("ftp://mozilla.com/");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: TEST_URI_1, transition: TRANSITION_TYPED },
+ { uri: TEST_URI_2, transition: TRANSITION_TYPED }
+ ]);
+
+ do_check_true(isHostInMozHosts(TEST_URI_1, true, "ftp://"));
+});
+
+add_task(function* test_moz_hosts_ftp_not_matchall()
+{
+ const TEST_URI_1 = NetUtil.newURI("http://mozilla.com/");
+ const TEST_URI_2 = NetUtil.newURI("ftp://mozilla.com/");
+
+ yield PlacesTestUtils.addVisits([
+ { uri: TEST_URI_1, transition: TRANSITION_TYPED },
+ { uri: TEST_URI_2, transition: TRANSITION_TYPED }
+ ]);
+
+ do_check_true(isHostInMozHosts(TEST_URI_1, true, null));
+});
+
+add_task(function* test_moz_hosts_update_2()
+{
+ // Check that updating trigger takes into account prefixes for different
+ // rev_hosts.
+ const TEST_URI_1 = NetUtil.newURI("https://www.google.it/");
+ const TEST_URI_2 = NetUtil.newURI("https://google.it/");
+ let places = [{ uri: TEST_URI_1
+ , transition: TRANSITION_TYPED
+ },
+ { uri: TEST_URI_2
+ }];
+ yield PlacesTestUtils.addVisits(places);
+
+ do_check_true(isHostInMozHosts(TEST_URI_1, true, "https://www."));
+});
+
+function run_test()
+{
+ run_next_test();
+}
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 000000000..771a6ac17
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_import_mobile_bookmarks.js
@@ -0,0 +1,292 @@
+function* importFromFixture(fixture, replace) {
+ let cwd = yield OS.File.getCurrentDirectory();
+ let path = OS.Path.join(cwd, fixture);
+
+ do_print(`Importing from ${path}`);
+ yield BookmarkJSONUtils.importFromFile(path, replace);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+}
+
+function* treeEquals(guid, expected, message) {
+ let root = yield 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);
+ }
+ if (node.annos) {
+ entry.annos = node.annos;
+ }
+ return entry;
+ }(root));
+
+ do_print(`Checking if ${guid} tree matches ${JSON.stringify(expected)}`);
+ do_print(`Got bookmarks tree for ${guid}: ${JSON.stringify(bookmarks)}`);
+
+ deepEqual(bookmarks, expected, message);
+}
+
+add_task(function* test_restore_mobile_bookmarks_root() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ true);
+
+ yield* 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,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ }],
+ }, "Should restore mobile bookmarks from root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_root() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_root_merge.json",
+ /* replace */ false);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "Utodo9b0oVws", index: 0 },
+ { guid: "X6lUyOspVYwi", index: 1 },
+ ],
+ }, {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1,
+ }, {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 3,
+ }, {
+ guid: PlacesUtils.bookmarks.mobileGuid,
+ index: 4,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "a17yW6-nTxEJ", index: 0 },
+ { guid: "xV10h9Wi3FBM", index: 1 },
+ { guid: "_o8e1_zxTJFg", index: 2 },
+ { guid: "QCtSqkVYUbXB", index: 3 },
+ ],
+ }],
+ }, "Should merge bookmarks root contents");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_mobile_bookmarks_folder() {
+ yield* importFromFixture("mobile_bookmarks_folder_import.json",
+ /* replace */ true);
+
+ yield* 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,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "_o8e1_zxTJFg", index: 0 },
+ { guid: "QCtSqkVYUbXB", index: 1 },
+ ],
+ }],
+ }, "Should restore mobile bookmark folder contents into mobile root");
+
+ // We rewrite queries to point to the root ID instead of the name
+ // ("MOBILE_BOOKMARKS") so that we don't break them if the user downgrades
+ // to an earlier release channel. This can be removed along with the anno in
+ // bug 1306445.
+ let queryById = yield PlacesUtils.bookmarks.fetch("XF4yRP6bTuil");
+ equal(queryById.url.href, "place:folder=" + PlacesUtils.mobileFolderId,
+ "Should rewrite mobile query to point to root ID");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_mobile_bookmarks_folder() {
+ yield* importFromFixture("mobile_bookmarks_folder_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_folder_merge.json",
+ /* replace */ false);
+
+ yield* treeEquals(PlacesUtils.bookmarks.rootGuid, {
+ guid: PlacesUtils.bookmarks.rootGuid,
+ index: 0,
+ children: [{
+ guid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ children: [
+ { guid: "Utodo9b0oVws", index: 0 },
+ { guid: "X6lUyOspVYwi", index: 1 },
+ { guid: "XF4yRP6bTuil", 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,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "a17yW6-nTxEJ", index: 0 },
+ { guid: "xV10h9Wi3FBM", index: 1 },
+ { guid: "_o8e1_zxTJFg", index: 2 },
+ { guid: "QCtSqkVYUbXB", index: 3 },
+ ],
+ }],
+ }, "Should merge bookmarks folder contents into mobile root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_restore_multiple_bookmarks_folders() {
+ yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+ /* replace */ true);
+
+ yield* 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,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "sSZ86WT9WbN3", index: 0 },
+ { guid: "a17yW6-nTxEJ", index: 1 },
+ ],
+ }],
+ }, "Should restore multiple bookmarks folder contents into root");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_import_multiple_bookmarks_folders() {
+ yield* importFromFixture("mobile_bookmarks_root_import.json",
+ /* replace */ false);
+ yield* importFromFixture("mobile_bookmarks_multiple_folders.json",
+ /* replace */ false);
+
+ yield* 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: "X6lUyOspVYwi", 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,
+ annos: [{
+ name: "mobile/bookmarksRoot",
+ flags: 0,
+ expires: 4,
+ value: 1,
+ }],
+ children: [
+ { guid: "sSZ86WT9WbN3", index: 0 },
+ { guid: "a17yW6-nTxEJ", index: 1 },
+ { guid: "_o8e1_zxTJFg", index: 2 },
+ { guid: "QCtSqkVYUbXB", index: 3 },
+ ],
+ }],
+ }, "Should merge multiple mobile folders into root");
+
+ yield 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 000000000..249853fa9
--- /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(function* test_execute() {
+ var good_uri = uri("http://mozilla.com");
+ var bad_uri = uri("http://google.com");
+ yield PlacesTestUtils.addVisits({uri: good_uri});
+ do_check_true(yield PlacesTestUtils.isPageInDB(good_uri));
+ do_check_false(yield 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 000000000..93c010e83
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isURIVisited.js
@@ -0,0 +1,84 @@
+/* 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,
+ "moz-anno:favicon:http://": false,
+ "view-source:http://": false,
+ "chrome://browser/content/browser.xul?": false,
+ "resource://": false,
+ "data:,": false,
+ "wyciwyg:/0/http://": false,
+ "javascript:": false,
+};
+
+var gRunner;
+function run_test()
+{
+ do_test_pending();
+ gRunner = step();
+ gRunner.next();
+}
+
+function* step()
+{
+ let history = Cc["@mozilla.org/browser/history;1"]
+ .getService(Ci.mozIAsyncHistory);
+
+ for (let scheme in SCHEMES) {
+ do_print("Testing scheme " + scheme);
+ for (let t in PlacesUtils.history.TRANSITIONS) {
+ do_print("With transition " + t);
+ let transition = PlacesUtils.history.TRANSITIONS[t];
+
+ let uri = NetUtil.newURI(scheme + "mozilla.org/");
+
+ history.isURIVisited(uri, function(aURI, aIsVisited) {
+ do_check_true(uri.equals(aURI));
+ do_check_false(aIsVisited);
+
+ let callback = {
+ handleError: function () {},
+ handleResult: function () {},
+ handleCompletion: function () {
+ do_print("Added visit to " + uri.spec);
+
+ history.isURIVisited(uri, function (aURI2, aIsVisited2) {
+ do_check_true(uri.equals(aURI2));
+ let checker = SCHEMES[scheme] ? do_check_true : do_check_false;
+ checker(aIsVisited2);
+
+ PlacesTestUtils.clearHistory().then(function () {
+ history.isURIVisited(uri, function(aURI3, aIsVisited3) {
+ do_check_true(uri.equals(aURI3));
+ do_check_false(aIsVisited3);
+ gRunner.next();
+ });
+ });
+ });
+ },
+ };
+
+ history.updatePlaces({ uri: uri
+ , visits: [ { transitionType: transition
+ , visitDate: Date.now() * 1000
+ } ]
+ }, callback);
+ });
+ yield undefined;
+ }
+ }
+
+ do_test_finished();
+}
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 000000000..d7bcc2851
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_isvisited.js
@@ -0,0 +1,75 @@
+/* -*- 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()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ var referrer = uri("about:blank");
+
+ // add a http:// uri
+ var uri1 = uri("http://mozilla.com");
+ yield PlacesTestUtils.addVisits({uri: uri1, referrer: referrer});
+ do_check_guid_for_uri(uri1);
+ do_check_true(yield promiseIsURIVisited(uri1));
+
+ // add a https:// uri
+ var uri2 = uri("https://etrade.com");
+ yield PlacesTestUtils.addVisits({uri: uri2, referrer: referrer});
+ do_check_guid_for_uri(uri2);
+ do_check_true(yield promiseIsURIVisited(uri2));
+
+ // add a ftp:// uri
+ var uri3 = uri("ftp://ftp.mozilla.org");
+ yield PlacesTestUtils.addVisits({uri: uri3, referrer: referrer});
+ do_check_guid_for_uri(uri3);
+ do_check_true(yield promiseIsURIVisited(uri3));
+
+ // check if a nonexistent uri is visited
+ var uri4 = uri("http://foobarcheese.com");
+ do_check_false(yield promiseIsURIVisited(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",
+ "moz-anno:favicon:http://mozilla.org/made-up-favicon",
+ "view-source:http://mozilla.org",
+ "chrome://browser/content/browser.xul",
+ "resource://gre-resources/hiddenWindow.html",
+ "data:,Hello%2C%20World!",
+ "wyciwyg:/0/http://mozilla.org",
+ "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.
+ do_print("Could not construct URI for '" + currentURL + "'; ignoring");
+ }
+ if (cantAddUri) {
+ PlacesTestUtils.addVisits({uri: cantAddUri, referrer: referrer}).then(() => {
+ do_throw("Should not have added history for invalid URI.");
+ }, error => {
+ do_check_true(error.message.includes("No items were added to history"));
+ });
+ do_check_false(yield promiseIsURIVisited(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 000000000..57b734c5d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_keywords.js
@@ -0,0 +1,548 @@
+"use strict"
+
+function* check_keyword(aExpectExists, aHref, aKeyword, aPostData = null) {
+ // Check case-insensitivity.
+ aKeyword = aKeyword.toUpperCase();
+
+ let entry = yield PlacesUtils.keywords.fetch(aKeyword);
+
+ Assert.deepEqual(entry, yield PlacesUtils.keywords.fetch({ keyword: aKeyword }));
+
+ if (aExpectExists) {
+ Assert.ok(!!entry, "A keyword should exist");
+ Assert.equal(entry.url.href, aHref);
+ Assert.equal(entry.postData, aPostData);
+ Assert.deepEqual(entry, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }));
+ let entries = [];
+ yield PlacesUtils.keywords.fetch({ url: aHref }, e => entries.push(e));
+ Assert.ok(entries.some(e => e.url.href == aHref && e.keyword == aKeyword.toLowerCase()));
+ } else {
+ Assert.ok(!entry || entry.url.href != aHref,
+ "The given keyword entry should not exist");
+ Assert.equal(null, yield PlacesUtils.keywords.fetch({ keyword: aKeyword, url: aHref }));
+ }
+}
+
+/**
+ * Polls the keywords cache waiting for the given keyword entry.
+ */
+function* promiseKeyword(keyword, expectedHref) {
+ let href = null;
+ do {
+ yield new Promise(resolve => do_timeout(100, resolve));
+ let entry = yield PlacesUtils.keywords.fetch(keyword);
+ if (entry)
+ href = entry.url.href;
+ } while (href != expectedHref);
+}
+
+function* check_no_orphans() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.executeCached(
+ `SELECT id FROM moz_keywords k
+ WHERE NOT EXISTS (SELECT 1 FROM moz_places WHERE id = k.place_id)
+ `);
+ Assert.equal(rows.length, 0);
+}
+
+function expectBookmarkNotifications() {
+ let notifications = [];
+ let observer = new Proxy(NavBookmarkObserver, {
+ get(target, name) {
+ if (name == "check") {
+ PlacesUtils.bookmarks.removeObserver(observer);
+ return expectedNotifications =>
+ Assert.deepEqual(notifications, expectedNotifications);
+ }
+
+ if (name.startsWith("onItemChanged")) {
+ return function(itemId, property) {
+ if (property != "keyword")
+ return;
+ let args = Array.from(arguments, arg => {
+ if (arg && arg instanceof Ci.nsIURI)
+ return new URL(arg.spec);
+ if (arg && typeof(arg) == "number" && arg >= Date.now() * 1000)
+ return new Date(parseInt(arg/1000));
+ return arg;
+ });
+ notifications.push({ name: name, arguments: args });
+ }
+ }
+
+ if (name in target)
+ return target[name];
+ return undefined;
+ }
+ });
+ PlacesUtils.bookmarks.addObserver(observer, false);
+ return observer;
+}
+
+add_task(function* test_invalid_input() {
+ Assert.throws(() => PlacesUtils.keywords.fetch(null),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch(5),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch(undefined),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: null }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: {} }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: 5 }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({}),
+ /At least keyword or url must be provided/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ keyword: "test" }, "test"),
+ /onResult callback must be a valid function/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: "test" }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: {} }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: null }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.fetch({ url: "" }),
+ /is not a valid URL/);
+
+ Assert.throws(() => PlacesUtils.keywords.insert(null),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.keywords.insert("test"),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.keywords.insert(undefined),
+ /Input should be a valid object/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: null }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: 5 }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "" }),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: 5 }),
+ /Invalid POST data/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", postData: {} }),
+ /Invalid POST data/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test" }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: 5 }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "" }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: null }),
+ /is not a valid URL/);
+ Assert.throws(() => PlacesUtils.keywords.insert({ keyword: "test", url: "mozilla" }),
+ /is not a valid URL/);
+
+ Assert.throws(() => PlacesUtils.keywords.remove(null),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.remove(""),
+ /Invalid keyword/);
+ Assert.throws(() => PlacesUtils.keywords.remove(5),
+ /Invalid keyword/);
+});
+
+add_task(function* test_addKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+ let observer = expectBookmarkNotifications();
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ observer.check([]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword
+
+ // Check using URL.
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: new URL("http://example.com/") });
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield PlacesUtils.keywords.remove("keyword");
+ yield check_keyword(false, "http://example.com/", "keyword");
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_addBookmarkAndKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+
+ let observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "keyword",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // -1 keyword
+
+ // Add again the keyword, then remove the bookmark.
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.bookmarks.remove(bookmark.guid);
+ // the notification is synchronous but the removal process is async.
+ // Unfortunately there's nothing explicit we can wait for.
+ while ((yield foreign_count("http://example.com/")));
+ // We don't get any itemChanged notification since the bookmark has been
+ // removed already.
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_addKeywordToURIHavingKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ observer.check([]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" });
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 keyword
+ let entries = [];
+ let entry = yield PlacesUtils.keywords.fetch({ url: "http://example.com/" }, e => entries.push(e));
+ Assert.equal(entries.length, 2);
+ Assert.deepEqual(entries[0], entry);
+
+ // Now remove the keywords.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ yield PlacesUtils.keywords.remove("keyword2");
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+ yield check_keyword(false, "http://example.com/", "keyword2");
+ Assert.equal((yield foreign_count("http://example.com/")), fc); // -1 keyword
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_addBookmarkToURIHavingKeyword() {
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let fc = yield foreign_count("http://example.com/");
+ let observer = expectBookmarkNotifications();
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ observer.check([]);
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 1); // +1 keyword
+
+ observer = expectBookmarkNotifications();
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark
+ observer.check([]);
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.bookmarks.remove(bookmark.guid);
+ // the notification is synchronous but the removal process is async.
+ // Unfortunately there's nothing explicit we can wait for.
+ while ((yield foreign_count("http://example.com/")));
+ // We don't get any itemChanged notification since the bookmark has been
+ // removed already.
+ observer.check([]);
+
+ yield check_keyword(false, "http://example.com/", "keyword");
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_sameKeywordDifferentURL() {
+ let fc1 = yield foreign_count("http://example1.com/");
+ let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let fc2 = yield foreign_count("http://example2.com/");
+ let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example2.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example1.com/" });
+
+ yield check_keyword(true, "http://example1.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword
+ yield check_keyword(false, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // +1 bookmark
+
+ // Assign the same keyword to another url.
+ let observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example2.com/" });
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+ "keyword", false, "",
+ bookmark1.lastModified, bookmark1.type,
+ (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+ bookmark1.guid, bookmark1.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "keyword",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(false, "http://example1.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1); // -1 keyword
+ yield check_keyword(true, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 keyword
+
+ // Now remove the keyword.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ yield check_keyword(false, "http://example1.com/", "keyword");
+ yield check_keyword(false, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 1);
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 1); // -1 keyword
+
+ yield PlacesUtils.bookmarks.remove(bookmark1);
+ yield PlacesUtils.bookmarks.remove(bookmark2);
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark
+ while ((yield foreign_count("http://example2.com/"))); // -1 keyword
+
+ yield check_no_orphans();
+});
+
+add_task(function* test_sameURIDifferentKeyword() {
+ let fc = yield foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({keyword: "keyword", url: "http://example.com/" });
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // +1 bookmark +1 keyword
+
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "keyword",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/" });
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +1 keyword
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "keyword2",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ // Add a third keyword.
+ yield PlacesUtils.keywords.insert({ keyword: "keyword3", url: "http://example.com/" });
+ yield check_keyword(true, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ yield check_keyword(true, "http://example.com/", "keyword3");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 4); // +1 keyword
+
+ // Remove one of the keywords.
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ yield check_keyword(false, "http://example.com/", "keyword");
+ yield check_keyword(true, "http://example.com/", "keyword2");
+ yield check_keyword(true, "http://example.com/", "keyword3");
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark.guid)),
+ "keyword", false, "",
+ bookmark.lastModified, bookmark.type,
+ (yield PlacesUtils.promiseItemId(bookmark.parentGuid)),
+ bookmark.guid, bookmark.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // -1 keyword
+
+ // Now remove the bookmark.
+ yield PlacesUtils.bookmarks.remove(bookmark);
+ while ((yield foreign_count("http://example.com/")));
+ yield check_keyword(false, "http://example.com/", "keyword");
+ yield check_keyword(false, "http://example.com/", "keyword2");
+ yield check_keyword(false, "http://example.com/", "keyword3");
+
+ check_no_orphans();
+});
+
+add_task(function* test_deleteKeywordMultipleBookmarks() {
+ let fc = yield foreign_count("http://example.com/");
+
+ let observer = expectBookmarkNotifications();
+ let bookmark1 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ let bookmark2 = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+
+ yield check_keyword(true, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 3); // +2 bookmark +1 keyword
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "keyword",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+ "keyword", false, "keyword",
+ bookmark1.lastModified, bookmark1.type,
+ (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+ bookmark1.guid, bookmark1.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ observer = expectBookmarkNotifications();
+ yield PlacesUtils.keywords.remove("keyword");
+ yield check_keyword(false, "http://example.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example.com/")), fc + 2); // -1 keyword
+ observer.check([{ name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark2.guid)),
+ "keyword", false, "",
+ bookmark2.lastModified, bookmark2.type,
+ (yield PlacesUtils.promiseItemId(bookmark2.parentGuid)),
+ bookmark2.guid, bookmark2.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] },
+ { name: "onItemChanged",
+ arguments: [ (yield PlacesUtils.promiseItemId(bookmark1.guid)),
+ "keyword", false, "",
+ bookmark1.lastModified, bookmark1.type,
+ (yield PlacesUtils.promiseItemId(bookmark1.parentGuid)),
+ bookmark1.guid, bookmark1.parentGuid, "",
+ Ci.nsINavBookmarksService.SOURCE_DEFAULT ] } ]);
+
+ // Now remove the bookmarks.
+ yield PlacesUtils.bookmarks.remove(bookmark1);
+ yield PlacesUtils.bookmarks.remove(bookmark2);
+ Assert.equal((yield foreign_count("http://example.com/")), fc); // -2 bookmarks
+
+ check_no_orphans();
+});
+
+add_task(function* test_multipleKeywordsSamePostData() {
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/", postData: "postData1" });
+ yield check_keyword(true, "http://example.com/", "keyword", "postData1");
+ // Add another keyword with same postData, should fail.
+ yield Assert.rejects(PlacesUtils.keywords.insert({ keyword: "keyword2", url: "http://example.com/", postData: "postData1" }),
+ /constraint failed/);
+ yield check_keyword(false, "http://example.com/", "keyword2", "postData1");
+
+ yield PlacesUtils.keywords.remove("keyword");
+
+ check_no_orphans();
+});
+
+add_task(function* test_oldPostDataAPI() {
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com/" });
+ let itemId = yield PlacesUtils.promiseItemId(bookmark.guid);
+ yield PlacesUtils.setPostDataForBookmark(itemId, "postData");
+ yield check_keyword(true, "http://example.com/", "keyword", "postData");
+ Assert.equal(PlacesUtils.getPostDataForBookmark(itemId), "postData");
+
+ yield PlacesUtils.keywords.remove("keyword");
+ yield PlacesUtils.bookmarks.remove(bookmark);
+
+ check_no_orphans();
+});
+
+add_task(function* test_oldKeywordsAPI() {
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield check_keyword(false, "http://example.com/", "keyword");
+ let itemId = yield PlacesUtils.promiseItemId(bookmark.guid);
+
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "keyword");
+ yield promiseKeyword("keyword", "http://example.com/");
+
+ // Remove the keyword.
+ PlacesUtils.bookmarks.setKeywordForBookmark(itemId, "");
+ yield promiseKeyword("keyword", null);
+
+ yield PlacesUtils.keywords.insert({ keyword: "keyword", url: "http://example.com" });
+ Assert.equal(PlacesUtils.bookmarks.getKeywordForBookmark(itemId), "keyword");
+ Assert.equal(PlacesUtils.bookmarks.getURIForKeyword("keyword").spec, "http://example.com/");
+ yield PlacesUtils.bookmarks.remove(bookmark);
+
+ check_no_orphans();
+});
+
+add_task(function* test_bookmarkURLChange() {
+ let fc1 = yield foreign_count("http://example1.com/");
+ let fc2 = yield foreign_count("http://example2.com/");
+ let bookmark = yield PlacesUtils.bookmarks.insert({ url: "http://example1.com/",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ yield PlacesUtils.keywords.insert({ keyword: "keyword",
+ url: "http://example1.com/" });
+
+ yield check_keyword(true, "http://example1.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1 + 2); // +1 bookmark +1 keyword
+
+ yield PlacesUtils.bookmarks.update({ guid: bookmark.guid,
+ url: "http://example2.com/"});
+ yield promiseKeyword("keyword", "http://example2.com/");
+
+ yield check_keyword(false, "http://example1.com/", "keyword");
+ yield check_keyword(true, "http://example2.com/", "keyword");
+ Assert.equal((yield foreign_count("http://example1.com/")), fc1); // -1 bookmark -1 keyword
+ Assert.equal((yield foreign_count("http://example2.com/")), fc2 + 2); // +1 bookmark +1 keyword
+});
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 000000000..c75494932
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_lastModified.js
@@ -0,0 +1,34 @@
+/* -*- 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 that inserting a new bookmark will set lastModified to the same
+ * values as dateAdded.
+ */
+// main
+function run_test() {
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ var itemId = bs.insertBookmark(bs.bookmarksMenuFolder,
+ uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX,
+ "itemTitle");
+ var dateAdded = bs.getItemDateAdded(itemId);
+ do_check_eq(dateAdded, bs.getItemLastModified(itemId));
+
+ // 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.
+ bs.setItemLastModified(itemId, dateAdded + 1000);
+ do_check_true(bs.getItemLastModified(itemId) === dateAdded + 1000);
+ do_check_true(bs.getItemDateAdded(itemId) < bs.getItemLastModified(itemId));
+ bs.setItemDateAdded(itemId, dateAdded + 2000);
+ do_check_true(bs.getItemDateAdded(itemId) === dateAdded + 2000);
+ do_check_eq(bs.getItemDateAdded(itemId), bs.getItemLastModified(itemId));
+
+ bs.removeItem(itemId);
+}
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 000000000..ba4f740c6
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_markpageas.js
@@ -0,0 +1,61 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+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}];
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ let observer;
+ let completionPromise = new Promise(resolveCompletionPromise => {
+ observer = {
+ __proto__: NavHistoryObserver.prototype,
+ _visitCount: 0,
+ onVisit: function (aURI, aVisitID, aTime, aSessionID, aReferringID,
+ aTransitionType, aAdded)
+ {
+ do_check_eq(aURI.spec, gVisits[this._visitCount].url);
+ do_check_eq(aTransitionType, gVisits[this._visitCount].transition);
+ this._visitCount++;
+
+ if (this._visitCount == gVisits.length) {
+ resolveCompletionPromise();
+ }
+ },
+ };
+ });
+
+ PlacesUtils.history.addObserver(observer, false);
+
+ 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
+ }
+ yield PlacesTestUtils.addVisits({
+ uri: uri(visit.url),
+ transition: visit.transition
+ });
+ }
+
+ yield completionPromise;
+
+ PlacesUtils.history.removeObserver(observer);
+});
+
diff --git a/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
new file mode 100644
index 000000000..5136591ba
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_mozIAsyncLivemarks.js
@@ -0,0 +1,514 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests functionality of the mozIAsyncLivemarks interface.
+
+const FEED_URI = NetUtil.newURI("http://feed.rss/");
+const SITE_URI = NetUtil.newURI("http://site.org/");
+
+// This test must be the first one, since it's testing the cache.
+add_task(function* test_livemark_cache() {
+ // Add a livemark through other APIs.
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "test",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ });
+ let id = yield PlacesUtils.promiseItemId(folder.guid);
+ PlacesUtils.annotations
+ .setItemAnnotation(id, PlacesUtils.LMANNO_FEEDURI,
+ "http://example.com/feed",
+ 0, PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations
+ .setItemAnnotation(id, PlacesUtils.LMANNO_SITEURI,
+ "http://example.com/site",
+ 0, PlacesUtils.annotations.EXPIRE_NEVER);
+
+ let livemark = yield PlacesUtils.livemarks.getLivemark({ guid: folder.guid });
+ Assert.equal(folder.guid, livemark.guid);
+ Assert.equal(folder.dateAdded * 1000, livemark.dateAdded);
+ Assert.equal(folder.parentGuid, livemark.parentGuid);
+ Assert.equal(folder.index, livemark.index);
+ Assert.equal(folder.title, livemark.title);
+ Assert.equal(id, livemark.id);
+ Assert.equal(PlacesUtils.unfiledBookmarksFolderId, livemark.parentId);
+ Assert.equal("http://example.com/feed", livemark.feedURI.spec);
+ Assert.equal("http://example.com/site", livemark.siteURI.spec);
+
+ yield PlacesUtils.livemarks.removeLivemark(livemark);
+});
+
+add_task(function* test_addLivemark_noArguments_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark();
+ do_throw("Invoking addLivemark with no arguments should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_XPC_NOT_ENOUGH_ARGS);
+ }
+});
+
+add_task(function* test_addLivemark_emptyObject_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({});
+ do_throw("Invoking addLivemark with empty object should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badParentId_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({ parentId: "test" });
+ do_throw("Invoking addLivemark with a bad parent id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_invalidParentId_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({ parentId: -2 });
+ do_throw("Invoking addLivemark with an invalid parent id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_noIndex_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark({
+ parentId: PlacesUtils.unfiledBookmarksFolderId });
+ do_throw("Invoking addLivemark with no index should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badIndex_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentId: PlacesUtils.unfiledBookmarksFolderId
+ , index: "test" });
+ do_throw("Invoking addLivemark with a bad index should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_invalidIndex_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentId: PlacesUtils.unfiledBookmarksFolderId
+ , index: -2
+ });
+ do_throw("Invoking addLivemark with an invalid index should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_noFeedURI_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfiledGuid });
+ do_throw("Invoking addLivemark with no feedURI should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badFeedURI_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: "test" });
+ do_throw("Invoking addLivemark with a bad feedURI should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badSiteURI_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , siteURI: "test" });
+ do_throw("Invoking addLivemark with a bad siteURI should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_badGuid_throws() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { parentGuid: PlacesUtils.bookmarks.unfileGuid
+ , feedURI: FEED_URI
+ , guid: "123456" });
+ do_throw("Invoking addLivemark with a bad guid should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_parentId_succeeds() {
+ let onItemAddedCalled = false;
+ PlacesUtils.bookmarks.addObserver({
+ __proto__: NavBookmarkObserver.prototype,
+ onItemAdded: function onItemAdded(aItemId, aParentId, aIndex, aItemType,
+ aURI, aTitle)
+ {
+ onItemAddedCalled = true;
+ PlacesUtils.bookmarks.removeObserver(this);
+ do_check_eq(aParentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(aIndex, 0);
+ do_check_eq(aItemType, Ci.nsINavBookmarksService.TYPE_FOLDER);
+ do_check_eq(aTitle, "test");
+ }
+ }, false);
+
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentId: PlacesUtils.unfiledBookmarksFolderId
+ , feedURI: FEED_URI });
+ do_check_true(onItemAddedCalled);
+});
+
+
+add_task(function* test_addLivemark_noSiteURI_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+ do_check_true(livemark.id > 0);
+ do_check_valid_places_guid(livemark.guid);
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_true(livemark.lastModified > 0);
+ do_check_true(is_time_ordered(livemark.dateAdded, livemark.lastModified));
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch(livemark.guid);
+ do_check_eq(livemark.index, bookmark.index);
+ do_check_eq(livemark.dateAdded, bookmark.dateAdded * 1000);
+});
+
+add_task(function* test_addLivemark_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , siteURI: SITE_URI
+ });
+
+ do_check_true(livemark.id > 0);
+ do_check_valid_places_guid(livemark.guid);
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_true(livemark.siteURI.equals(SITE_URI));
+ do_check_true(PlacesUtils.annotations
+ .itemHasAnnotation(livemark.id,
+ PlacesUtils.LMANNO_FEEDURI));
+ do_check_true(PlacesUtils.annotations
+ .itemHasAnnotation(livemark.id,
+ PlacesUtils.LMANNO_SITEURI));
+});
+
+add_task(function* test_addLivemark_bogusid_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { id: 100 // Should be ignored.
+ , title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , siteURI: SITE_URI
+ });
+ do_check_true(livemark.id > 0);
+ do_check_neq(livemark.id, 100);
+});
+
+add_task(function* test_addLivemark_bogusParentId_fails() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentId: 187
+ , feedURI: FEED_URI
+ });
+ do_throw("Adding a livemark with a bogus parent should fail");
+ } catch (ex) {}
+});
+
+add_task(function* test_addLivemark_bogusParentGuid_fails() {
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: "123456789012"
+ , feedURI: FEED_URI
+ });
+ do_throw("Adding a livemark with a bogus parent should fail");
+ } catch (ex) {}
+})
+
+add_task(function* test_addLivemark_intoLivemark_fails() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ try {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: livemark.guid
+ , feedURI: FEED_URI
+ });
+ do_throw("Adding a livemark into a livemark should fail");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_addLivemark_forceGuid_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , guid: "1234567890AB"
+ });
+ do_check_eq(livemark.guid, "1234567890AB");
+ do_check_guid_for_bookmark(livemark.id, "1234567890AB");
+});
+
+add_task(function* test_addLivemark_dateAdded_succeeds() {
+ let dateAdded = new Date("2013-03-01T01:10:00") * 1000;
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , dateAdded
+ });
+ do_check_eq(livemark.dateAdded, dateAdded);
+});
+
+add_task(function* test_addLivemark_lastModified_succeeds() {
+ let now = Date.now() * 1000;
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , lastModified: now
+ });
+ do_check_eq(livemark.dateAdded, now);
+ do_check_eq(livemark.lastModified, now);
+});
+
+add_task(function* test_removeLivemark_emptyObject_throws() {
+ try {
+ yield PlacesUtils.livemarks.removeLivemark({});
+ do_throw("Invoking removeLivemark with empty object should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_removeLivemark_noValidId_throws() {
+ try {
+ yield PlacesUtils.livemarks.removeLivemark({ id: -10, guid: "test"});
+ do_throw("Invoking removeLivemark with no valid id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_removeLivemark_nonExistent_fails() {
+ try {
+ yield PlacesUtils.livemarks.removeLivemark({ id: 1337 });
+ do_throw("Removing a non-existent livemark should fail");
+ }
+ catch (ex) {
+ }
+});
+
+add_task(function* test_removeLivemark_guid_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , guid: "234567890ABC"
+ });
+
+ do_check_eq(livemark.guid, "234567890ABC");
+
+ yield PlacesUtils.livemarks.removeLivemark({
+ id: 789, guid: "234567890ABC"
+ });
+
+ do_check_eq((yield PlacesUtils.bookmarks.fetch("234567890ABC")), null);
+});
+
+add_task(function* test_removeLivemark_id_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ yield PlacesUtils.livemarks.removeLivemark({ id: livemark.id });
+
+ do_check_eq((yield PlacesUtils.bookmarks.fetch("234567890ABC")), null);
+});
+
+add_task(function* test_getLivemark_emptyObject_throws() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({});
+ do_throw("Invoking getLivemark with empty object should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_getLivemark_noValidId_throws() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ id: -10, guid: "test"});
+ do_throw("Invoking getLivemark with no valid id should throw");
+ } catch (ex) {
+ do_check_eq(ex.result, Cr.NS_ERROR_INVALID_ARG);
+ }
+});
+
+add_task(function* test_getLivemark_nonExistentId_fails() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ id: 1234 });
+ do_throw("getLivemark for a non existent id should fail");
+ } catch (ex) {}
+});
+
+add_task(function* test_getLivemark_nonExistentGUID_fails() {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ guid: "34567890ABCD" });
+ do_throw("getLivemark for a non-existent guid should fail");
+ } catch (ex) {}
+});
+
+add_task(function* test_getLivemark_guid_succeeds() {
+ yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ , guid: "34567890ABCD" });
+
+ // invalid id to check the guid wins.
+ let livemark =
+ yield PlacesUtils.livemarks.getLivemark({ id: 789, guid: "34567890ABCD" });
+
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_eq(livemark.guid, "34567890ABCD");
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch("34567890ABCD");
+ do_check_eq(livemark.index, bookmark.index);
+});
+
+add_task(function* test_getLivemark_id_succeeds() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ livemark = yield PlacesUtils.livemarks.getLivemark({ id: livemark.id });
+
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_eq(livemark.parentGuid, PlacesUtils.bookmarks.unfiledGuid);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_guid_for_bookmark(livemark.id, livemark.guid);
+
+ let bookmark = yield PlacesUtils.bookmarks.fetch(livemark.guid);
+ do_check_eq(livemark.index, bookmark.index);
+});
+
+add_task(function* test_getLivemark_removeItem_contention() {
+ // do not yield.
+ PlacesUtils.livemarks.addLivemark({ title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ }).catch(() => { /* swallow errors*/ });
+ yield PlacesUtils.bookmarks.eraseEverything();
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ livemark = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+
+ do_check_eq(livemark.title, "test");
+ do_check_eq(livemark.parentId, PlacesUtils.unfiledBookmarksFolderId);
+ do_check_true(livemark.feedURI.equals(FEED_URI));
+ do_check_eq(livemark.siteURI, null);
+ do_check_guid_for_bookmark(livemark.id, livemark.guid);
+});
+
+add_task(function* test_title_change() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI
+ });
+
+ yield PlacesUtils.bookmarks.update({ guid: livemark.guid,
+ title: "test2" });
+ // Poll for the title change.
+ while (true) {
+ let lm = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ if (lm.title == "test2")
+ break;
+ yield new Promise(resolve => do_timeout(resolve, 100));
+ }
+});
+
+add_task(function* test_livemark_move() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI } );
+
+ yield PlacesUtils.bookmarks.update({ guid: livemark.guid,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX });
+ // Poll for the parent change.
+ while (true) {
+ let lm = yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ if (lm.parentGuid == PlacesUtils.bookmarks.toolbarGuid)
+ break;
+ yield new Promise(resolve => do_timeout(resolve, 100));
+ }
+});
+
+add_task(function* test_livemark_removed() {
+ let livemark = yield PlacesUtils.livemarks.addLivemark(
+ { title: "test"
+ , parentGuid: PlacesUtils.bookmarks.unfiledGuid
+ , feedURI: FEED_URI } );
+
+ yield PlacesUtils.bookmarks.remove(livemark.guid);
+ // Poll for the livemark removal.
+ while (true) {
+ try {
+ yield PlacesUtils.livemarks.getLivemark({ guid: livemark.guid });
+ } catch (ex) {
+ break;
+ }
+ yield new Promise(resolve => do_timeout(resolve, 100));
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_multi_queries.js b/toolkit/components/places/tests/unit/test_multi_queries.js
new file mode 100644
index 000000000..d485355a5
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_multi_queries.js
@@ -0,0 +1,53 @@
+/* -*- 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/. */
+
+/**
+ * Adds a test URI visit to history.
+ *
+ * @param aURI
+ * The URI to add a visit for.
+ * @param aReferrer
+ * The referring URI for the given URI. This can be null.
+ */
+function* add_visit(aURI, aDayOffset, aTransition) {
+ yield PlacesTestUtils.addVisits({
+ uri: aURI,
+ transition: aTransition,
+ visitDate: (Date.now() + aDayOffset*86400000) * 1000
+ });
+}
+
+function run_test()
+{
+ run_next_test();
+}
+
+add_task(function* test_execute()
+{
+ yield add_visit(uri("http://mirror1.mozilla.com/a"), -1, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror2.mozilla.com/b"), -2, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror3.mozilla.com/c"), -4, TRANSITION_FRAMED_LINK);
+ yield add_visit(uri("http://mirror1.google.com/b"), -1, TRANSITION_EMBED);
+ yield add_visit(uri("http://mirror2.google.com/a"), -2, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror1.apache.org/b"), -3, TRANSITION_LINK);
+ yield add_visit(uri("http://mirror2.apache.org/a"), -4, TRANSITION_FRAMED_LINK);
+
+ let queries = [
+ PlacesUtils.history.getNewQuery(),
+ PlacesUtils.history.getNewQuery()
+ ];
+ queries[0].domain = "mozilla.com";
+ queries[1].domain = "google.com";
+
+ let root = PlacesUtils.history.executeQueries(
+ queries, queries.length, PlacesUtils.history.getNewQueryOptions()
+ ).root;
+ root.containerOpen = true;
+ let childCount = root.childCount;
+ root.containerOpen = false;
+
+ do_check_eq(childCount, 3);
+});
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 000000000..6a0e5f130
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_multi_word_tags.js
@@ -0,0 +1,150 @@
+/* -*- 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 bookmark service
+try {
+ var bmsvc = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+}
+catch (ex) {
+ do_throw("Could not get the nav-bookmarks-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");
+}
+
+// main
+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");
+
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri1, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri2, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri3, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri4, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri5, bmsvc.DEFAULT_INDEX, null);
+ bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri6, bmsvc.DEFAULT_INDEX, null);
+
+ 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"]);
+
+ // exclude livemark items, 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;
+ do_check_eq(root.childCount, 3);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/1");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/4");
+ do_check_eq(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;
+ do_check_eq(root.childCount, 4);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/2");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/4");
+ do_check_eq(root.getChild(2).uri, "http://site.tld/5");
+ do_check_eq(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;
+ do_check_eq(root.childCount, 3);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/3");
+ do_check_eq(root.getChild(1).uri, "http://site.tld/5");
+ do_check_eq(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;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/4");
+ do_check_eq(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;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/4");
+ do_check_eq(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;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/5");
+ do_check_eq(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;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/5");
+ do_check_eq(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;
+ do_check_eq(root.childCount, 1);
+ do_check_eq(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;
+ do_check_eq(root.childCount, 1);
+ do_check_eq(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;
+ do_check_eq(root.childCount, 1);
+ do_check_eq(root.getChild(0).uri, "http://site.tld/6");
+ root.containerOpen = false;
+}
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 000000000..037ab7d08
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_nsINavHistoryViewer.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/. */
+
+// Get history service
+var histsvc = PlacesUtils.history;
+var bhist = PlacesUtils.bhistory;
+var bmsvc = PlacesUtils.bookmarks;
+
+var resultObserver = {
+ insertedNode: null,
+ nodeInserted: function(parent, node, newIndex) {
+ this.insertedNode = node;
+ },
+ removedNode: null,
+ nodeRemoved: function(parent, node, oldIndex) {
+ this.removedNode = node;
+ },
+
+ nodeAnnotationChanged: function() {},
+
+ newTitle: "",
+ nodeChangedByTitle: null,
+ nodeTitleChanged: function(node, newTitle) {
+ this.nodeChangedByTitle = node;
+ this.newTitle = newTitle;
+ },
+
+ newAccessCount: 0,
+ newTime: 0,
+ nodeChangedByHistoryDetails: null,
+ nodeHistoryDetailsChanged: function(node,
+ updatedVisitDate,
+ updatedVisitCount) {
+ this.nodeChangedByHistoryDetails = node
+ this.newTime = updatedVisitDate;
+ this.newAccessCount = updatedVisitCount;
+ },
+
+ movedNode: null,
+ nodeMoved: function(node, oldParent, oldIndex, newParent, newIndex) {
+ this.movedNode = node;
+ },
+ openedContainer: null,
+ closedContainer: null,
+ containerStateChanged: function (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: function(node) {
+ this.invalidatedContainer = node;
+ },
+ sortingMode: null,
+ sortingChanged: function(sortingMode) {
+ this.sortingMode = sortingMode;
+ },
+ inBatchMode: false,
+ batching: function(aToggleMode) {
+ do_check_neq(this.inBatchMode, aToggleMode);
+ this.inBatchMode = aToggleMode;
+ },
+ result: null,
+ reset: function() {
+ 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;
+ }
+};
+
+var testURI = uri("http://mozilla.com");
+
+function run_test() {
+ run_next_test();
+}
+
+add_test(function check_history_query() {
+ var options = histsvc.getNewQueryOptions();
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+ options.resultType = options.RESULTS_AS_VISIT;
+ var query = histsvc.getNewQuery();
+ var result = histsvc.executeQuery(query, options);
+ result.addObserver(resultObserver, false);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_neq(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.nodeInserted
+ // add a visit
+ PlacesTestUtils.addVisits(testURI).then(function() {
+ do_check_eq(testURI.spec, resultObserver.insertedNode.uri);
+
+ // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
+ // adding a visit causes nodeHistoryDetailsChanged for the folder
+ do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+
+ // nsINavHistoryResultObserver.itemTitleChanged for a leaf node
+ PlacesTestUtils.addVisits({ uri: testURI, title: "baz" }).then(function () {
+ do_check_eq(resultObserver.nodeChangedByTitle.title, "baz");
+
+ // nsINavHistoryResultObserver.nodeRemoved
+ var removedURI = uri("http://google.com");
+ PlacesTestUtils.addVisits(removedURI).then(function() {
+ bhist.removePage(removedURI);
+ do_check_eq(removedURI.spec, resultObserver.removedNode.uri);
+
+ // nsINavHistoryResultObserver.invalidateContainer
+ bhist.removePagesFromHost("mozilla.com", false);
+ do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
+
+ // nsINavHistoryResultObserver.sortingChanged
+ resultObserver.invalidatedContainer = null;
+ result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
+ do_check_eq(resultObserver.invalidatedContainer, result.root);
+
+ // nsINavHistoryResultObserver.invalidateContainer
+ PlacesTestUtils.clearHistoryEnabled().then(() => {
+ do_check_eq(root.uri, resultObserver.invalidatedContainer.uri);
+
+ // nsINavHistoryResultObserver.batching
+ do_check_false(resultObserver.inBatchMode);
+ histsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+ bmsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+
+ root.containerOpen = false;
+ do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ PlacesTestUtils.promiseAsyncUpdates().then(run_next_test);
+ });
+ });
+ });
+ });
+});
+
+add_test(function check_bookmarks_query() {
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.setFolders([bmsvc.bookmarksMenuFolder], 1);
+ var result = histsvc.executeQuery(query, options);
+ result.addObserver(resultObserver, false);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_neq(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.nodeInserted
+ // add a bookmark
+ var testBookmark = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, testURI, bmsvc.DEFAULT_INDEX, "foo");
+ do_check_eq("foo", resultObserver.insertedNode.title);
+ do_check_eq(testURI.spec, resultObserver.insertedNode.uri);
+
+ // nsINavHistoryResultObserver.nodeHistoryDetailsChanged
+ // adding a visit causes nodeHistoryDetailsChanged for the folder
+ do_check_eq(root.uri, resultObserver.nodeChangedByHistoryDetails.uri);
+
+ // nsINavHistoryResultObserver.nodeTitleChanged for a leaf node
+ bmsvc.setItemTitle(testBookmark, "baz");
+ do_check_eq(resultObserver.nodeChangedByTitle.title, "baz");
+ do_check_eq(resultObserver.newTitle, "baz");
+
+ var testBookmark2 = bmsvc.insertBookmark(bmsvc.bookmarksMenuFolder, uri("http://google.com"), bmsvc.DEFAULT_INDEX, "foo");
+ bmsvc.moveItem(testBookmark2, bmsvc.bookmarksMenuFolder, 0);
+ do_check_eq(resultObserver.movedNode.itemId, testBookmark2);
+
+ // nsINavHistoryResultObserver.nodeRemoved
+ bmsvc.removeItem(testBookmark2);
+ do_check_eq(testBookmark2, resultObserver.removedNode.itemId);
+
+ // XXX nsINavHistoryResultObserver.invalidateContainer
+
+ // nsINavHistoryResultObserver.sortingChanged
+ resultObserver.invalidatedContainer = null;
+ result.sortingMode = options.SORT_BY_TITLE_ASCENDING;
+ do_check_eq(resultObserver.sortingMode, options.SORT_BY_TITLE_ASCENDING);
+ do_check_eq(resultObserver.invalidatedContainer, result.root);
+
+ // nsINavHistoryResultObserver.batching
+ do_check_false(resultObserver.inBatchMode);
+ histsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+ bmsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+
+ root.containerOpen = false;
+ do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ PlacesTestUtils.promiseAsyncUpdates().then(run_next_test);
+});
+
+add_test(function check_mixed_query() {
+ var options = histsvc.getNewQueryOptions();
+ var query = histsvc.getNewQuery();
+ query.onlyBookmarked = true;
+ var result = histsvc.executeQuery(query, options);
+ result.addObserver(resultObserver, false);
+ var root = result.root;
+ root.containerOpen = true;
+
+ do_check_neq(resultObserver.openedContainer, null);
+
+ // nsINavHistoryResultObserver.batching
+ do_check_false(resultObserver.inBatchMode);
+ histsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+ bmsvc.runInBatchMode({
+ runBatched: function (aUserData) {
+ do_check_true(resultObserver.inBatchMode);
+ }
+ }, null);
+ do_check_false(resultObserver.inBatchMode);
+
+ root.containerOpen = false;
+ do_check_eq(resultObserver.closedContainer, resultObserver.openedContainer);
+ result.removeObserver(resultObserver);
+ resultObserver.reset();
+ PlacesTestUtils.promiseAsyncUpdates().then(run_next_test);
+});
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 000000000..524837ca3
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_null_interfaces.js
@@ -0,0 +1,98 @@
+/* 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"],
+ ["queryStringToQueries", "removePagesByTimeframe", "removePagesFromHost", "getObservers"]
+ ],
+ ["browser/nav-bookmarks-service;1",
+ ["nsINavBookmarksService", "nsINavHistoryObserver", "nsIAnnotationObserver"],
+ ["createFolder", "getObservers", "onFrecencyChanged", "onTitleChanged",
+ "onPageAnnotationSet", "onPageAnnotationRemoved", "onDeleteURI"]
+ ],
+ ["browser/livemark-service;2", ["mozIAsyncLivemarks"], ["reloadLivemarks"]],
+ ["browser/annotation-service;1", ["nsIAnnotationService"], []],
+ ["browser/favicon-service;1", ["nsIFaviconService"], []],
+ ["browser/tagging-service;1", ["nsITaggingService"], []],
+];
+do_print(testServices.join("\n"));
+
+function run_test()
+{
+ for (let [cid, ifaces, nothrow] of testServices) {
+ do_print(`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) {
+ do_print(`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 == 0)
+ mesg = "No args needed!";
+ else if (name == "QueryInterface")
+ mesg = "Ignore QI!";
+
+ if (mesg) {
+ do_print(`${mesg} Skipping: ${name}`);
+ return false;
+ }
+
+ return true;
+ }
+
+ do_print(`Generating an array of functions to test service: ${s}`);
+ for (let n of Object.keys(s).filter(i => okName(i)).sort()) {
+ do_print(`\nTesting ${ifaces.join(", ")} function with null args: ${n}`);
+
+ let func = s[n];
+ let num = func.length;
+ do_print(`Generating array of nulls for #args: ${num}`);
+ let args = Array(num).fill(null);
+
+ let tryAgain = true;
+ while (tryAgain == true) {
+ try {
+ do_print(`Calling with args: ${JSON.stringify(args)}`);
+ func.apply(s, args);
+
+ do_print(`The function did not throw! Is it one of the nothrow? ${nothrow}`);
+ Assert.notEqual(nothrow.indexOf(n), -1);
+
+ do_print("Must have been an expected nothrow, so no need to try again");
+ tryAgain = false;
+ }
+ catch (ex) {
+ if (ex.result == Cr.NS_ERROR_ILLEGAL_VALUE) {
+ do_print(`Caught an expected exception: ${ex.name}`);
+ do_print("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]);
+ do_print(`Function call expects an out object at ${pos}`);
+ args[pos] = {};
+ } else if (ex.result == Cr.NS_ERROR_NOT_IMPLEMENTED) {
+ do_print(`Method not implemented exception: ${ex.name}`);
+ do_print("Moving on to the next test..");
+ tryAgain = false;
+ } else {
+ throw ex;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/toolkit/components/places/tests/unit/test_onItemChanged_tags.js b/toolkit/components/places/tests/unit/test_onItemChanged_tags.js
new file mode 100644
index 000000000..7a0eb354d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_onItemChanged_tags.js
@@ -0,0 +1,52 @@
+/* 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 OnItemChanged("tags") only once, and not once per tag.
+
+function run_test() {
+ do_test_pending();
+
+ let tags = ["a", "b", "c"];
+ let uri = NetUtil.newURI("http://1.moz.org/");
+
+ let id = PlacesUtils.bookmarks.insertBookmark(
+ PlacesUtils.unfiledBookmarksFolderId, uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX, "Bookmark 1"
+ );
+ PlacesUtils.tagging.tagURI(uri, tags);
+
+ let bookmarksObserver = {
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsINavBookmarkObserver
+ ]),
+
+ _changedCount: 0,
+ onItemChanged: function (aItemId, aProperty, aIsAnnotationProperty, aValue,
+ aLastModified, aItemType) {
+ if (aProperty == "tags") {
+ do_check_eq(aItemId, id);
+ this._changedCount++;
+ }
+ },
+
+ onItemRemoved: function (aItemId, aParentId, aIndex, aItemType) {
+ if (aItemId == id) {
+ PlacesUtils.bookmarks.removeObserver(this);
+ do_check_eq(this._changedCount, 2);
+ do_test_finished();
+ }
+ },
+
+ onItemAdded: function () {},
+ onBeginUpdateBatch: function () {},
+ onEndUpdateBatch: function () {},
+ onItemVisited: function () {},
+ onItemMoved: function () {},
+ };
+ PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
+
+ PlacesUtils.tagging.tagURI(uri, ["d"]);
+ PlacesUtils.tagging.tagURI(uri, ["e"]);
+ PlacesUtils.bookmarks.removeItem(id);
+}
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 000000000..f6131b211
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_pageGuid_bookmarkGuid.js
@@ -0,0 +1,179 @@
+/* -*- 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 histsvc = PlacesUtils.history;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test_addBookmarksAndCheckGuids() {
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX);
+ bmsvc.insertBookmark(folder, uri("http://test1.com/"),
+ bmsvc.DEFAULT_INDEX, "1 title");
+ bmsvc.insertBookmark(folder, uri("http://test2.com/"),
+ bmsvc.DEFAULT_INDEX, "2 title");
+ bmsvc.insertBookmark(folder, uri("http://test3.com/"),
+ bmsvc.DEFAULT_INDEX, "3 title");
+ bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX);
+ bmsvc.createFolder(folder, "test folder 2", bmsvc.DEFAULT_INDEX);
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ do_check_eq(root.childCount, 5);
+
+ // check bookmark guids
+ let bookmarkGuidZero = root.getChild(0).bookmarkGuid;
+ do_check_eq(bookmarkGuidZero.length, 12);
+ // bookmarks have bookmark guids
+ do_check_eq(root.getChild(1).bookmarkGuid.length, 12);
+ do_check_eq(root.getChild(2).bookmarkGuid.length, 12);
+ // separator has bookmark guid
+ do_check_eq(root.getChild(3).bookmarkGuid.length, 12);
+ // folder has bookmark guid
+ do_check_eq(root.getChild(4).bookmarkGuid.length, 12);
+ // all bookmark guids are different.
+ do_check_neq(bookmarkGuidZero, root.getChild(1).bookmarkGuid);
+ do_check_neq(root.getChild(1).bookmarkGuid, root.getChild(2).bookmarkGuid);
+ do_check_neq(root.getChild(2).bookmarkGuid, root.getChild(3).bookmarkGuid);
+ do_check_neq(root.getChild(3).bookmarkGuid, root.getChild(4).bookmarkGuid);
+
+ // check page guids
+ let pageGuidZero = root.getChild(0).pageGuid;
+ do_check_eq(pageGuidZero.length, 12);
+ // bookmarks have page guids
+ do_check_eq(root.getChild(1).pageGuid.length, 12);
+ do_check_eq(root.getChild(2).pageGuid.length, 12);
+ // folder and separator don't have page guids
+ do_check_eq(root.getChild(3).pageGuid, "");
+ do_check_eq(root.getChild(4).pageGuid, "");
+
+ do_check_neq(pageGuidZero, root.getChild(1).pageGuid);
+ do_check_neq(root.getChild(1).pageGuid, root.getChild(2).pageGuid);
+
+ root.containerOpen = false;
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_updateBookmarksAndCheckGuids() {
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder", bmsvc.DEFAULT_INDEX);
+ let b1 = bmsvc.insertBookmark(folder, uri("http://test1.com/"),
+ bmsvc.DEFAULT_INDEX, "1 title");
+ let f1 = bmsvc.createFolder(folder, "test folder 2", bmsvc.DEFAULT_INDEX);
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ do_check_eq(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;
+ bmsvc.setItemTitle(b1, "1 title mod");
+ do_check_eq(root.getChild(0).title, "1 title mod");
+ do_check_eq(root.getChild(0).bookmarkGuid, bookmarkGuidZero);
+ do_check_eq(root.getChild(0).pageGuid, pageGuidZero);
+
+ let bookmarkGuidOne = root.getChild(1).bookmarkGuid;
+ let pageGuidOne = root.getChild(1).pageGuid;
+ bmsvc.setItemTitle(f1, "test foolder 234");
+ do_check_eq(root.getChild(1).title, "test foolder 234");
+ do_check_eq(root.getChild(1).bookmarkGuid, bookmarkGuidOne);
+ do_check_eq(root.getChild(1).pageGuid, pageGuidOne);
+
+ root.containerOpen = false;
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_addVisitAndCheckGuid() {
+ // add a visit and test page guid and non-existing bookmark guids.
+ let sourceURI = uri("http://test4.com/");
+ yield PlacesTestUtils.addVisits({ uri: sourceURI });
+ do_check_eq(bmsvc.getBookmarkedURIFor(sourceURI), null);
+
+ let options = histsvc.getNewQueryOptions();
+ let query = histsvc.getNewQuery();
+ query.uri = sourceURI;
+ let root = histsvc.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 1);
+
+ do_check_valid_places_guid(root.getChild(0).pageGuid);
+ do_check_eq(root.getChild(0).bookmarkGuid, "");
+ root.containerOpen = false;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_addItemsWithInvalidGUIDsFails() {
+ const INVALID_GUID = "XYZ";
+ try {
+ bmsvc.createFolder(bmsvc.placesRoot, "XYZ folder",
+ bmsvc.DEFAULT_INDEX, INVALID_GUID);
+ do_throw("Adding a folder with an invalid guid should fail");
+ }
+ catch (ex) { }
+
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX);
+ try {
+ bmsvc.insertBookmark(folder, uri("http://test.tld"), bmsvc.DEFAULT_INDEX,
+ "title", INVALID_GUID);
+ do_throw("Adding a bookmark with an invalid guid should fail");
+ }
+ catch (ex) { }
+
+ try {
+ bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX, INVALID_GUID);
+ do_throw("Adding a separator with an invalid guid should fail");
+ }
+ catch (ex) { }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_addItemsWithGUIDs() {
+ const FOLDER_GUID = "FOLDER--GUID";
+ const BOOKMARK_GUID = "BM------GUID";
+ const SEPARATOR_GUID = "SEP-----GUID";
+
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX, FOLDER_GUID);
+ bmsvc.insertBookmark(folder, uri("http://test1.com/"), bmsvc.DEFAULT_INDEX,
+ "1 title", BOOKMARK_GUID);
+ bmsvc.insertSeparator(folder, bmsvc.DEFAULT_INDEX, SEPARATOR_GUID);
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ do_check_eq(root.childCount, 2);
+ do_check_eq(root.bookmarkGuid, FOLDER_GUID);
+ do_check_eq(root.getChild(0).bookmarkGuid, BOOKMARK_GUID);
+ do_check_eq(root.getChild(1).bookmarkGuid, SEPARATOR_GUID);
+
+ root.containerOpen = false;
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_emptyGUIDIgnored() {
+ let folder = bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX, "");
+ do_check_valid_places_guid(PlacesUtils.getFolderContents(folder)
+ .root.bookmarkGuid);
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_usingSameGUIDFails() {
+ const GUID = "XYZXYZXYZXYZ";
+ bmsvc.createFolder(bmsvc.placesRoot, "test folder",
+ bmsvc.DEFAULT_INDEX, GUID);
+ try {
+ bmsvc.createFolder(bmsvc.placesRoot, "test folder 2",
+ bmsvc.DEFAULT_INDEX, GUID);
+ do_throw("Using the same guid twice should fail");
+ }
+ catch (ex) { }
+
+ yield 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 000000000..0f585ca51
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_placeURIs.js
@@ -0,0 +1,42 @@
+/* -*- 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");
+}
+
+// main
+function run_test() {
+ // XXX Full testing coverage for QueriesToQueryString and
+ // QueryStringToQueries
+
+ var bs = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
+ getService(Ci.nsINavBookmarksService);
+ const NHQO = Ci.nsINavHistoryQueryOptions;
+ // Bug 376798
+ var query = histsvc.getNewQuery();
+ query.setFolders([bs.placesRoot], 1);
+ do_check_eq(histsvc.queriesToQueryString([query], 1, histsvc.getNewQueryOptions()),
+ "place:folder=PLACES_ROOT");
+
+ // Bug 378828
+ var options = histsvc.getNewQueryOptions();
+ options.sortingAnnotation = "test anno";
+ options.sortingMode = NHQO.SORT_BY_ANNOTATION_DESCENDING;
+ var placeURI =
+ "place:folder=PLACES_ROOT&sort=" + NHQO.SORT_BY_ANNOTATION_DESCENDING +
+ "&sortingAnnotation=test%20anno";
+ do_check_eq(histsvc.queriesToQueryString([query], 1, options),
+ placeURI);
+ options = {};
+ histsvc.queryStringToQueries(placeURI, { }, {}, options);
+ do_check_eq(options.value.sortingAnnotation, "test anno");
+ do_check_eq(options.value.sortingMode, NHQO.SORT_BY_ANNOTATION_DESCENDING);
+}
diff --git a/toolkit/components/places/tests/unit/test_placesTxn.js b/toolkit/components/places/tests/unit/test_placesTxn.js
new file mode 100644
index 000000000..3cc9809bb
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_placesTxn.js
@@ -0,0 +1,937 @@
+/* -*- 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 bmsvc = PlacesUtils.bookmarks;
+var tagssvc = PlacesUtils.tagging;
+var annosvc = PlacesUtils.annotations;
+var txnManager = PlacesUtils.transactionManager;
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+
+function* promiseKeyword(keyword, href, postData) {
+ while (true) {
+ let entry = yield PlacesUtils.keywords.fetch(keyword);
+ if (href == null && !entry)
+ break;
+ if (entry && entry.url.href == href && entry.postData == postData) {
+ break;
+ }
+
+ yield new Promise(resolve => do_timeout(100, resolve));
+ }
+}
+
+// create and add bookmarks observer
+var observer = {
+
+ onBeginUpdateBatch: function() {
+ this._beginUpdateBatch = true;
+ },
+ _beginUpdateBatch: false,
+
+ onEndUpdateBatch: function() {
+ this._endUpdateBatch = true;
+ },
+ _endUpdateBatch: false,
+
+ onItemAdded: function(id, folder, index, itemType, uri) {
+ this._itemAddedId = id;
+ this._itemAddedParent = folder;
+ this._itemAddedIndex = index;
+ this._itemAddedType = itemType;
+ },
+ _itemAddedId: null,
+ _itemAddedParent: null,
+ _itemAddedIndex: null,
+ _itemAddedType: null,
+
+ onItemRemoved: function(id, folder, index, itemType) {
+ this._itemRemovedId = id;
+ this._itemRemovedFolder = folder;
+ this._itemRemovedIndex = index;
+ },
+ _itemRemovedId: null,
+ _itemRemovedFolder: null,
+ _itemRemovedIndex: null,
+
+ onItemChanged: function(id, property, isAnnotationProperty, newValue,
+ lastModified, itemType) {
+ // The transaction manager is being rewritten in bug 891303, so just
+ // skip checking this for now.
+ if (property == "tags")
+ return;
+ this._itemChangedId = id;
+ this._itemChangedProperty = property;
+ this._itemChanged_isAnnotationProperty = isAnnotationProperty;
+ this._itemChangedValue = newValue;
+ },
+ _itemChangedId: null,
+ _itemChangedProperty: null,
+ _itemChanged_isAnnotationProperty: null,
+ _itemChangedValue: null,
+
+ onItemVisited: function(id, visitID, time) {
+ this._itemVisitedId = id;
+ this._itemVisitedVistId = visitID;
+ this._itemVisitedTime = time;
+ },
+ _itemVisitedId: null,
+ _itemVisitedVistId: null,
+ _itemVisitedTime: null,
+
+ onItemMoved: function(id, oldParent, oldIndex, newParent, newIndex,
+ itemType) {
+ this._itemMovedId = id;
+ this._itemMovedOldParent = oldParent;
+ this._itemMovedOldIndex = oldIndex;
+ this._itemMovedNewParent = newParent;
+ this._itemMovedNewIndex = newIndex;
+ },
+ _itemMovedId: null,
+ _itemMovedOldParent: null,
+ _itemMovedOldIndex: null,
+ _itemMovedNewParent: null,
+ _itemMovedNewIndex: null,
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsINavBookmarkObserver) ||
+ iid.equals(Ci.nsISupports)) {
+ return this;
+ }
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+// index at which items should begin
+var bmStartIndex = 0;
+
+// get bookmarks root id
+var root = PlacesUtils.bookmarksMenuFolderId;
+
+add_task(function* init() {
+ bmsvc.addObserver(observer, false);
+ do_register_cleanup(function () {
+ bmsvc.removeObserver(observer);
+ });
+});
+
+add_task(function* test_create_folder_with_description() {
+ const TEST_FOLDERNAME = "Test creating a folder with a description";
+ const TEST_DESCRIPTION = "this is my test description";
+
+ let annos = [{ name: DESCRIPTION_ANNO,
+ type: annosvc.TYPE_STRING,
+ flags: 0,
+ value: TEST_DESCRIPTION,
+ expires: annosvc.EXPIRE_NEVER }];
+ let txn = new PlacesCreateFolderTransaction(TEST_FOLDERNAME, root, bmStartIndex, annos);
+ txnManager.doTransaction(txn);
+
+ // This checks that calling undoTransaction on an "empty batch" doesn't
+ // undo the previous transaction (getItemTitle will fail)
+ txnManager.beginBatch(null);
+ txnManager.endBatch(false);
+ txnManager.undoTransaction();
+
+ let folderId = observer._itemAddedId;
+ do_check_eq(bmsvc.getItemTitle(folderId), TEST_FOLDERNAME);
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, root);
+ do_check_eq(observer._itemAddedId, folderId);
+ do_check_eq(TEST_DESCRIPTION, annosvc.getItemAnnotation(folderId, DESCRIPTION_ANNO));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, root);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, root);
+ do_check_eq(observer._itemAddedId, folderId);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, root);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+});
+
+add_task(function* test_create_item() {
+ let testURI = NetUtil.newURI("http://test_create_item.com");
+
+ let txn = new PlacesCreateBookmarkTransaction(testURI, root, bmStartIndex,
+ "Test creating an item");
+
+ txnManager.doTransaction(txn);
+ let id = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedId, id);
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_true(bmsvc.isBookmarked(testURI));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, id);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+ do_check_false(bmsvc.isBookmarked(testURI));
+
+ txn.redoTransaction();
+ do_check_true(bmsvc.isBookmarked(testURI));
+ let newId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, root);
+ do_check_eq(observer._itemAddedId, newId);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newId);
+ do_check_eq(observer._itemRemovedFolder, root);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+});
+
+add_task(function* test_create_item_to_folder() {
+ const TEST_FOLDERNAME = "Test creating item to a folder";
+ let testURI = NetUtil.newURI("http://test_create_item_to_folder.com");
+ let folderId = bmsvc.createFolder(root, TEST_FOLDERNAME, bmsvc.DEFAULT_INDEX);
+
+ let txn = new PlacesCreateBookmarkTransaction(testURI, folderId, bmStartIndex,
+ "Test creating item");
+ txnManager.doTransaction(txn);
+ let bkmId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedId, bkmId);
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_true(bmsvc.isBookmarked(testURI));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, bkmId);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+
+ txn.redoTransaction();
+ let newBkmId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedIndex, bmStartIndex);
+ do_check_eq(observer._itemAddedParent, folderId);
+ do_check_eq(observer._itemAddedId, newBkmId);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newBkmId);
+ do_check_eq(observer._itemRemovedFolder, folderId);
+ do_check_eq(observer._itemRemovedIndex, bmStartIndex);
+});
+
+add_task(function* test_move_items_to_folder() {
+ let testFolderId = bmsvc.createFolder(root, "Test move items", bmsvc.DEFAULT_INDEX);
+ let testURI = NetUtil.newURI("http://test_move_items.com");
+ let testBkmId = bmsvc.insertBookmark(testFolderId, testURI, bmsvc.DEFAULT_INDEX, "1: Test move items");
+ bmsvc.insertBookmark(testFolderId, testURI, bmsvc.DEFAULT_INDEX, "2: Test move items");
+
+ // Moving items between the same folder
+ let sameTxn = new PlacesMoveItemTransaction(testBkmId, testFolderId, bmsvc.DEFAULT_INDEX);
+
+ sameTxn.doTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 1);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 1);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ sameTxn.redoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 1);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 1);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ // Moving items between different folders
+ let folderId = bmsvc.createFolder(testFolderId,
+ "Test move items between different folders",
+ bmsvc.DEFAULT_INDEX);
+ let diffTxn = new PlacesMoveItemTransaction(testBkmId, folderId, bmsvc.DEFAULT_INDEX);
+
+ diffTxn.doTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, folderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, folderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ diffTxn.redoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, testFolderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, folderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+
+ sameTxn.undoTransaction();
+ do_check_eq(observer._itemMovedId, testBkmId);
+ do_check_eq(observer._itemMovedOldParent, folderId);
+ do_check_eq(observer._itemMovedOldIndex, 0);
+ do_check_eq(observer._itemMovedNewParent, testFolderId);
+ do_check_eq(observer._itemMovedNewIndex, 0);
+});
+
+add_task(function* test_remove_folder() {
+ let testFolder = bmsvc.createFolder(root, "Test Removing a Folder", bmsvc.DEFAULT_INDEX);
+ let folderId = bmsvc.createFolder(testFolder, "Removed Folder", bmsvc.DEFAULT_INDEX);
+
+ let txn = new PlacesRemoveItemTransaction(folderId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, folderId);
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, folderId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, folderId);
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+});
+
+add_task(function* test_remove_item_with_tag() {
+ // Notice in this case the tag persists since other bookmarks have same uri.
+ let testFolder = bmsvc.createFolder(root, "Test removing an item with a tag",
+ bmsvc.DEFAULT_INDEX);
+
+ const TAG_NAME = "tag-test_remove_item_with_tag";
+ let testURI = NetUtil.newURI("http://test_remove_item_with_tag.com");
+ let testBkmId = bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item1");
+
+ // create bookmark for not removing tag.
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item2");
+
+ // set tag
+ tagssvc.tagURI(testURI, [TAG_NAME]);
+
+ let txn = new PlacesRemoveItemTransaction(testBkmId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, testBkmId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI), TAG_NAME);
+
+ txn.undoTransaction();
+ let newbkmk2Id = observer._itemAddedId;
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, newbkmk2Id);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(tagssvc.getTagsForURI(testURI)[0], TAG_NAME);
+});
+
+add_task(function* test_remove_item_with_keyword() {
+ // Notice in this case the tag persists since other bookmarks have same uri.
+ let testFolder = bmsvc.createFolder(root, "Test removing an item with a keyword",
+ bmsvc.DEFAULT_INDEX);
+
+ const KEYWORD = "test: test removing an item with a keyword";
+ let testURI = NetUtil.newURI("http://test_remove_item_with_keyword.com");
+ let testBkmId = bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "test-item1");
+
+ // set keyword
+ yield PlacesUtils.keywords.insert({ url: testURI.spec, keyword: KEYWORD});
+
+ let txn = new PlacesRemoveItemTransaction(testBkmId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, testBkmId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ yield promiseKeyword(KEYWORD, null);
+
+ txn.undoTransaction();
+ let newbkmk2Id = observer._itemAddedId;
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+ yield promiseKeyword(KEYWORD, testURI.spec);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, newbkmk2Id);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+ yield promiseKeyword(KEYWORD, null);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+});
+
+add_task(function* test_creating_separator() {
+ let testFolder = bmsvc.createFolder(root, "Test creating a separator", bmsvc.DEFAULT_INDEX);
+
+ let txn = new PlacesCreateSeparatorTransaction(testFolder, 0);
+ txn.doTransaction();
+
+ let sepId = observer._itemAddedId;
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(observer._itemAddedParent, testFolder);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, sepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.redoTransaction();
+ let newSepId = observer._itemAddedId;
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(observer._itemAddedParent, testFolder);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newSepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+});
+
+add_task(function* test_removing_separator() {
+ let testFolder = bmsvc.createFolder(root, "Test removing a separator", bmsvc.DEFAULT_INDEX);
+
+ let sepId = bmsvc.insertSeparator(testFolder, 0);
+ let txn = new PlacesRemoveItemTransaction(sepId);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemRemovedId, sepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, sepId); // New separator created
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemRemovedId, sepId);
+ do_check_eq(observer._itemRemovedFolder, testFolder);
+ do_check_eq(observer._itemRemovedIndex, 0);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemAddedId, sepId); // New separator created
+ do_check_eq(observer._itemAddedParent, testFolder);
+ do_check_eq(observer._itemAddedIndex, 0);
+});
+
+add_task(function* test_editing_item_title() {
+ const TITLE = "Test editing item title";
+ const MOD_TITLE = "Mod: Test editing item title";
+ let testURI = NetUtil.newURI("http://www.test_editing_item_title.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, TITLE);
+
+ let txn = new PlacesEditItemTitleTransaction(testBkmId, MOD_TITLE);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, MOD_TITLE);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, TITLE);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, MOD_TITLE);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "title");
+ do_check_eq(observer._itemChangedValue, TITLE);
+});
+
+add_task(function* test_editing_item_uri() {
+ const OLD_TEST_URI = NetUtil.newURI("http://old.test_editing_item_uri.com/");
+ const NEW_TEST_URI = NetUtil.newURI("http://new.test_editing_item_uri.com/");
+ let testBkmId = bmsvc.insertBookmark(root, OLD_TEST_URI, bmsvc.DEFAULT_INDEX,
+ "Test editing item title");
+ tagssvc.tagURI(OLD_TEST_URI, ["tag"]);
+
+ let txn = new PlacesEditBookmarkURITransaction(testBkmId, NEW_TEST_URI);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, NEW_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify([]));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, OLD_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify([]));
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, NEW_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify([]));
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "uri");
+ do_check_eq(observer._itemChangedValue, OLD_TEST_URI.spec);
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(OLD_TEST_URI)), JSON.stringify(["tag"]));
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(NEW_TEST_URI)), JSON.stringify([]));
+});
+
+add_task(function* test_edit_description_transaction() {
+ let testURI = NetUtil.newURI("http://test_edit_description_transaction.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit description transaction");
+
+ let anno = {
+ name: DESCRIPTION_ANNO,
+ type: Ci.nsIAnnotationService.TYPE_STRING,
+ flags: 0,
+ value: "Test edit Description",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER,
+ };
+ let txn = new PlacesSetItemAnnotationTransaction(testBkmId, anno);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, DESCRIPTION_ANNO);
+});
+
+add_task(function* test_edit_keyword() {
+ const KEYWORD = "keyword-test_edit_keyword";
+
+ let testURI = NetUtil.newURI("http://test_edit_keyword.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword");
+
+ let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData");
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, KEYWORD);
+ do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData");
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, "");
+ do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), null);
+});
+
+add_task(function* test_edit_specific_keyword() {
+ const KEYWORD = "keyword-test_edit_keyword2";
+
+ let testURI = NetUtil.newURI("http://test_edit_keyword2.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test edit keyword");
+ // Add multiple keyword to this uri.
+ yield PlacesUtils.keywords.insert({ keyword: "kw1", url: testURI.spec, postData: "postData1" });
+ yield PlacesUtils.keywords.insert({keyword: "kw2", url: testURI.spec, postData: "postData2" });
+
+ // Try to change only kw2.
+ let txn = new PlacesEditBookmarkKeywordTransaction(testBkmId, KEYWORD, "postData2", "kw2");
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, KEYWORD);
+ let entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, testURI.spec);
+ Assert.equal(entry.postData, "postData1");
+ yield promiseKeyword(KEYWORD, testURI.spec, "postData2");
+ yield promiseKeyword("kw2", null);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "keyword");
+ do_check_eq(observer._itemChangedValue, "kw2");
+ do_check_eq(PlacesUtils.getPostDataForBookmark(testBkmId), "postData1");
+ entry = yield PlacesUtils.keywords.fetch("kw1");
+ Assert.equal(entry.url.href, testURI.spec);
+ Assert.equal(entry.postData, "postData1");
+ yield promiseKeyword("kw2", testURI.spec, "postData2");
+ yield promiseKeyword("keyword", null);
+});
+
+add_task(function* test_LoadInSidebar_transaction() {
+ let testURI = NetUtil.newURI("http://test_LoadInSidebar_transaction.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test LoadInSidebar transaction");
+
+ const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+ let anno = { name: LOAD_IN_SIDEBAR_ANNO,
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: true,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let txn = new PlacesSetItemAnnotationTransaction(testBkmId, anno);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, LOAD_IN_SIDEBAR_ANNO);
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, LOAD_IN_SIDEBAR_ANNO);
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+});
+
+add_task(function* test_generic_item_annotation() {
+ let testURI = NetUtil.newURI("http://test_generic_item_annotation.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX, "Test generic item annotation");
+
+ let itemAnnoObj = { name: "testAnno/testInt",
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: 123,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let txn = new PlacesSetItemAnnotationTransaction(testBkmId, itemAnnoObj);
+
+ txn.doTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+
+ txn.undoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+
+ txn.redoTransaction();
+ do_check_eq(observer._itemChangedId, testBkmId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+});
+
+add_task(function* test_editing_item_date_added() {
+ let testURI = NetUtil.newURI("http://test_editing_item_date_added.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
+ "Test editing item date added");
+
+ let oldAdded = bmsvc.getItemDateAdded(testBkmId);
+ let newAdded = Date.now() * 1000 + 1000;
+ let txn = new PlacesEditItemDateAddedTransaction(testBkmId, newAdded);
+
+ txn.doTransaction();
+ do_check_eq(newAdded, bmsvc.getItemDateAdded(testBkmId));
+
+ txn.undoTransaction();
+ do_check_eq(oldAdded, bmsvc.getItemDateAdded(testBkmId));
+});
+
+add_task(function* test_edit_item_last_modified() {
+ let testURI = NetUtil.newURI("http://test_edit_item_last_modified.com");
+ let testBkmId = bmsvc.insertBookmark(root, testURI, bmsvc.DEFAULT_INDEX,
+ "Test editing item last modified");
+
+ let oldModified = bmsvc.getItemLastModified(testBkmId);
+ let newModified = Date.now() * 1000 + 1000;
+ let txn = new PlacesEditItemLastModifiedTransaction(testBkmId, newModified);
+
+ txn.doTransaction();
+ do_check_eq(newModified, bmsvc.getItemLastModified(testBkmId));
+
+ txn.undoTransaction();
+ do_check_eq(oldModified, bmsvc.getItemLastModified(testBkmId));
+});
+
+add_task(function* test_generic_page_annotation() {
+ const TEST_ANNO = "testAnno/testInt";
+ let testURI = NetUtil.newURI("http://www.mozilla.org/");
+ PlacesTestUtils.addVisits(testURI).then(function () {
+ let pageAnnoObj = { name: TEST_ANNO,
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: 123,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let txn = new PlacesSetPageAnnotationTransaction(testURI, pageAnnoObj);
+
+ txn.doTransaction();
+ do_check_true(annosvc.pageHasAnnotation(testURI, TEST_ANNO));
+
+ txn.undoTransaction();
+ do_check_false(annosvc.pageHasAnnotation(testURI, TEST_ANNO));
+
+ txn.redoTransaction();
+ do_check_true(annosvc.pageHasAnnotation(testURI, TEST_ANNO));
+ });
+});
+
+add_task(function* test_sort_folder_by_name() {
+ let testFolder = bmsvc.createFolder(root, "Test PlacesSortFolderByNameTransaction",
+ bmsvc.DEFAULT_INDEX);
+ let testURI = NetUtil.newURI("http://test_sort_folder_by_name.com");
+
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark3");
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark2");
+ bmsvc.insertBookmark(testFolder, testURI, bmsvc.DEFAULT_INDEX, "bookmark1");
+
+ let bkmIds = bmsvc.getBookmarkIdsForURI(testURI);
+ bkmIds.sort();
+
+ let b1 = bkmIds[0];
+ let b2 = bkmIds[1];
+ let b3 = bkmIds[2];
+
+ do_check_eq(0, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(2, bmsvc.getItemIndex(b3));
+
+ let txn = new PlacesSortFolderByNameTransaction(testFolder);
+
+ txn.doTransaction();
+ do_check_eq(2, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(0, bmsvc.getItemIndex(b3));
+
+ txn.undoTransaction();
+ do_check_eq(0, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(2, bmsvc.getItemIndex(b3));
+
+ txn.redoTransaction();
+ do_check_eq(2, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(0, bmsvc.getItemIndex(b3));
+
+ txn.undoTransaction();
+ do_check_eq(0, bmsvc.getItemIndex(b1));
+ do_check_eq(1, bmsvc.getItemIndex(b2));
+ do_check_eq(2, bmsvc.getItemIndex(b3));
+});
+
+add_task(function* test_tagURI_untagURI() {
+ const TAG_1 = "tag-test_tagURI_untagURI-bar";
+ const TAG_2 = "tag-test_tagURI_untagURI-foo";
+ let tagURI = NetUtil.newURI("http://test_tagURI_untagURI.com");
+
+ // Test tagURI
+ let tagTxn = new PlacesTagURITransaction(tagURI, [TAG_1, TAG_2]);
+
+ tagTxn.doTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2]));
+
+ tagTxn.undoTransaction();
+ do_check_eq(tagssvc.getTagsForURI(tagURI).length, 0);
+
+ tagTxn.redoTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2]));
+
+ // Test untagURI
+ let untagTxn = new PlacesUntagURITransaction(tagURI, [TAG_1]);
+
+ untagTxn.doTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_2]));
+
+ untagTxn.undoTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_1, TAG_2]));
+
+ untagTxn.redoTransaction();
+ do_check_eq(JSON.stringify(tagssvc.getTagsForURI(tagURI)), JSON.stringify([TAG_2]));
+});
+
+add_task(function* test_aggregate_removeItem_Txn() {
+ let testFolder = bmsvc.createFolder(root, "Test aggregate removeItem transaction", bmsvc.DEFAULT_INDEX);
+
+ const TEST_URL = "http://test_aggregate_removeitem_txn.com/";
+ const FOLDERNAME = "Folder";
+ let testURI = NetUtil.newURI(TEST_URL);
+
+ let bkmk1Id = bmsvc.insertBookmark(testFolder, testURI, 0, "Mozilla");
+ let bkmk2Id = bmsvc.insertSeparator(testFolder, 1);
+ let bkmk3Id = bmsvc.createFolder(testFolder, FOLDERNAME, 2);
+
+ let bkmk3_1Id = bmsvc.insertBookmark(bkmk3Id, testURI, 0, "Mozilla");
+ let bkmk3_2Id = bmsvc.insertSeparator(bkmk3Id, 1);
+ let bkmk3_3Id = bmsvc.createFolder(bkmk3Id, FOLDERNAME, 2);
+
+ let childTxn1 = new PlacesRemoveItemTransaction(bkmk1Id);
+ let childTxn2 = new PlacesRemoveItemTransaction(bkmk2Id);
+ let childTxn3 = new PlacesRemoveItemTransaction(bkmk3Id);
+ let transactions = [childTxn1, childTxn2, childTxn3];
+ let txn = new PlacesAggregatedTransaction("RemoveItems", transactions);
+
+ txn.doTransaction();
+ do_check_eq(bmsvc.getItemIndex(bkmk1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3_1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3_2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(bkmk3_3Id), -1);
+ // Check last removed item id.
+ do_check_eq(observer._itemRemovedId, bkmk3Id);
+
+ txn.undoTransaction();
+ let newBkmk1Id = bmsvc.getIdForItemAt(testFolder, 0);
+ let newBkmk2Id = bmsvc.getIdForItemAt(testFolder, 1);
+ let newBkmk3Id = bmsvc.getIdForItemAt(testFolder, 2);
+ let newBkmk3_1Id = bmsvc.getIdForItemAt(newBkmk3Id, 0);
+ let newBkmk3_2Id = bmsvc.getIdForItemAt(newBkmk3Id, 1);
+ let newBkmk3_3Id = bmsvc.getIdForItemAt(newBkmk3Id, 2);
+ do_check_eq(bmsvc.getItemType(newBkmk1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getItemType(newBkmk2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getItemType(newBkmk3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3Id), FOLDERNAME);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_1Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk3_1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_2Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_3Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3_3Id), FOLDERNAME);
+ // Check last added back item id.
+ // Notice items are restored in reverse order.
+ do_check_eq(observer._itemAddedId, newBkmk1Id);
+
+ txn.redoTransaction();
+ do_check_eq(bmsvc.getItemIndex(newBkmk1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3_1Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3_2Id), -1);
+ do_check_eq(bmsvc.getItemIndex(newBkmk3_3Id), -1);
+ // Check last removed item id.
+ do_check_eq(observer._itemRemovedId, newBkmk3Id);
+
+ txn.undoTransaction();
+ newBkmk1Id = bmsvc.getIdForItemAt(testFolder, 0);
+ newBkmk2Id = bmsvc.getIdForItemAt(testFolder, 1);
+ newBkmk3Id = bmsvc.getIdForItemAt(testFolder, 2);
+ newBkmk3_1Id = bmsvc.getIdForItemAt(newBkmk3Id, 0);
+ newBkmk3_2Id = bmsvc.getIdForItemAt(newBkmk3Id, 1);
+ newBkmk3_3Id = bmsvc.getIdForItemAt(newBkmk3Id, 2);
+ do_check_eq(bmsvc.getItemType(newBkmk1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getItemType(newBkmk2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getItemType(newBkmk3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3Id), FOLDERNAME);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_1Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_1Id), bmsvc.TYPE_BOOKMARK);
+ do_check_eq(bmsvc.getBookmarkURI(newBkmk3_1Id).spec, TEST_URL);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_2Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_2Id), bmsvc.TYPE_SEPARATOR);
+ do_check_eq(bmsvc.getFolderIdForItem(newBkmk3_3Id), newBkmk3Id);
+ do_check_eq(bmsvc.getItemType(newBkmk3_3Id), bmsvc.TYPE_FOLDER);
+ do_check_eq(bmsvc.getItemTitle(newBkmk3_3Id), FOLDERNAME);
+ // Check last added back item id.
+ // Notice items are restored in reverse order.
+ do_check_eq(observer._itemAddedId, newBkmk1Id);
+});
+
+add_task(function* test_create_item_with_childTxn() {
+ let testFolder = bmsvc.createFolder(root, "Test creating an item with childTxns", bmsvc.DEFAULT_INDEX);
+
+ const BOOKMARK_TITLE = "parent item";
+ let testURI = NetUtil.newURI("http://test_create_item_with_childTxn.com");
+ let childTxns = [];
+ let newDateAdded = Date.now() * 1000 - 20000;
+ let editDateAdddedTxn = new PlacesEditItemDateAddedTransaction(null, newDateAdded);
+ childTxns.push(editDateAdddedTxn);
+
+ let itemChildAnnoObj = { name: "testAnno/testInt",
+ type: Ci.nsIAnnotationService.TYPE_INT32,
+ flags: 0,
+ value: 123,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER };
+ let annoTxn = new PlacesSetItemAnnotationTransaction(null, itemChildAnnoObj);
+ childTxns.push(annoTxn);
+
+ let itemWChildTxn = new PlacesCreateBookmarkTransaction(testURI, testFolder, bmStartIndex,
+ BOOKMARK_TITLE, null, null,
+ childTxns);
+ try {
+ txnManager.doTransaction(itemWChildTxn);
+ let itemId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(observer._itemAddedId, itemId);
+ do_check_eq(newDateAdded, bmsvc.getItemDateAdded(itemId));
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+ do_check_true(annosvc.itemHasAnnotation(itemId, itemChildAnnoObj.name))
+ do_check_eq(annosvc.getItemAnnotation(itemId, itemChildAnnoObj.name), itemChildAnnoObj.value);
+
+ itemWChildTxn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, itemId);
+
+ itemWChildTxn.redoTransaction();
+ do_check_true(bmsvc.isBookmarked(testURI));
+ let newId = bmsvc.getBookmarkIdsForURI(testURI)[0];
+ do_check_eq(newDateAdded, bmsvc.getItemDateAdded(newId));
+ do_check_eq(observer._itemAddedId, newId);
+ do_check_eq(observer._itemChangedProperty, "testAnno/testInt");
+ do_check_eq(observer._itemChanged_isAnnotationProperty, true);
+ do_check_true(annosvc.itemHasAnnotation(newId, itemChildAnnoObj.name))
+ do_check_eq(annosvc.getItemAnnotation(newId, itemChildAnnoObj.name), itemChildAnnoObj.value);
+
+ itemWChildTxn.undoTransaction();
+ do_check_eq(observer._itemRemovedId, newId);
+ }
+ catch (ex) {
+ do_throw("Setting a child transaction in a createItem transaction did throw: " + ex);
+ }
+});
+
+add_task(function* test_create_folder_with_child_itemTxn() {
+ let childURI = NetUtil.newURI("http://test_create_folder_with_child_itemTxn.com");
+ let childItemTxn = new PlacesCreateBookmarkTransaction(childURI, root,
+ bmStartIndex, "childItem");
+ let txn = new PlacesCreateFolderTransaction("Test creating a folder with child itemTxns",
+ root, bmStartIndex, null, [childItemTxn]);
+ try {
+ txnManager.doTransaction(txn);
+ let childItemId = bmsvc.getBookmarkIdsForURI(childURI)[0];
+ do_check_eq(observer._itemAddedId, childItemId);
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_true(bmsvc.isBookmarked(childURI));
+
+ txn.undoTransaction();
+ do_check_false(bmsvc.isBookmarked(childURI));
+
+ txn.redoTransaction();
+ let newchildItemId = bmsvc.getBookmarkIdsForURI(childURI)[0];
+ do_check_eq(observer._itemAddedIndex, 0);
+ do_check_eq(observer._itemAddedId, newchildItemId);
+ do_check_true(bmsvc.isBookmarked(childURI));
+
+ txn.undoTransaction();
+ do_check_false(bmsvc.isBookmarked(childURI));
+ }
+ catch (ex) {
+ do_throw("Setting a child item transaction in a createFolder transaction did throw: " + ex);
+ }
+});
diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance.js b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
new file mode 100644
index 000000000..a533c8295
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance.js
@@ -0,0 +1,1356 @@
+/* -*- 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 preventive maintenance
+ * For every maintenance query create an uncoherent db and check that we take
+ * correct fix steps, without polluting valid data.
+ */
+
+// Include PlacesDBUtils module
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+const FINISHED_MAINTENANCE_NOTIFICATION_TOPIC = "places-maintenance-finished";
+
+// Get services and database connection
+var hs = PlacesUtils.history;
+var bs = PlacesUtils.bookmarks;
+var ts = PlacesUtils.tagging;
+var as = PlacesUtils.annotations;
+var fs = PlacesUtils.favicons;
+
+var mDBConn = hs.QueryInterface(Ci.nsPIPlacesDatabase).DBConnection;
+
+// ------------------------------------------------------------------------------
+// Helpers
+
+var defaultBookmarksMaxId = 0;
+function cleanDatabase() {
+ mDBConn.executeSimpleSQL("DELETE FROM moz_places");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_historyvisits");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_anno_attributes");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_annos");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_items_annos");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_inputhistory");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_keywords");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_favicons");
+ mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE id > " + defaultBookmarksMaxId);
+}
+
+function addPlace(aUrl, aFavicon) {
+ let stmt = mDBConn.createStatement(
+ "INSERT INTO moz_places (url, url_hash, favicon_id) VALUES (:url, hash(:url), :favicon)");
+ stmt.params["url"] = aUrl || "http://www.mozilla.org";
+ stmt.params["favicon"] = aFavicon || null;
+ stmt.execute();
+ stmt.finalize();
+ return mDBConn.lastInsertRowID;
+}
+
+function addBookmark(aPlaceId, aType, aParent, aKeywordId, aFolderType, aTitle) {
+ let stmt = mDBConn.createStatement(
+ `INSERT INTO moz_bookmarks (fk, type, parent, keyword_id, folder_type,
+ title, guid)
+ VALUES (:place_id, :type, :parent, :keyword_id, :folder_type, :title,
+ GENERATE_GUID())`);
+ stmt.params["place_id"] = aPlaceId || null;
+ stmt.params["type"] = aType || bs.TYPE_BOOKMARK;
+ stmt.params["parent"] = aParent || bs.unfiledBookmarksFolder;
+ stmt.params["keyword_id"] = aKeywordId || null;
+ stmt.params["folder_type"] = aFolderType || null;
+ stmt.params["title"] = typeof(aTitle) == "string" ? aTitle : null;
+ stmt.execute();
+ stmt.finalize();
+ return mDBConn.lastInsertRowID;
+}
+
+// ------------------------------------------------------------------------------
+// Tests
+
+var tests = [];
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "A.1",
+ desc: "Remove obsolete annotations from moz_annos",
+
+ _obsoleteWeaveAttribute: "weave/test",
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid.
+ this._placeId = addPlace();
+ // Add an obsolete attribute.
+ let stmt = mDBConn.createStatement(
+ "INSERT INTO moz_anno_attributes (name) VALUES (:anno)"
+ );
+ stmt.params['anno'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement(
+ `INSERT INTO moz_annos (place_id, anno_attribute_id)
+ VALUES (:place_id,
+ (SELECT id FROM moz_anno_attributes WHERE name = :anno)
+ )`
+ );
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that the obsolete annotation has been removed.
+ let stmt = mDBConn.createStatement(
+ "SELECT id FROM moz_anno_attributes WHERE name = :anno"
+ );
+ stmt.params['anno'] = this._obsoleteWeaveAttribute;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+tests.push({
+ name: "A.2",
+ desc: "Remove obsolete annotations from moz_items_annos",
+
+ _obsoleteSyncAttribute: "sync/children",
+ _obsoleteGuidAttribute: "placesInternal/GUID",
+ _obsoleteWeaveAttribute: "weave/test",
+ _placeId: null,
+ _bookmarkId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid.
+ this._placeId = addPlace();
+ // Add a bookmark.
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add an obsolete attribute.
+ let stmt = mDBConn.createStatement(
+ `INSERT INTO moz_anno_attributes (name)
+ VALUES (:anno1), (:anno2), (:anno3)`
+ );
+ stmt.params['anno1'] = this._obsoleteSyncAttribute;
+ stmt.params['anno2'] = this._obsoleteGuidAttribute;
+ stmt.params['anno3'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement(
+ `INSERT INTO moz_items_annos (item_id, anno_attribute_id)
+ SELECT :item_id, id
+ FROM moz_anno_attributes
+ WHERE name IN (:anno1, :anno2, :anno3)`
+ );
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.params['anno1'] = this._obsoleteSyncAttribute;
+ stmt.params['anno2'] = this._obsoleteGuidAttribute;
+ stmt.params['anno3'] = this._obsoleteWeaveAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that the obsolete annotations have been removed.
+ let stmt = mDBConn.createStatement(
+ `SELECT id FROM moz_anno_attributes
+ WHERE name IN (:anno1, :anno2, :anno3)`
+ );
+ stmt.params['anno1'] = this._obsoleteSyncAttribute;
+ stmt.params['anno2'] = this._obsoleteGuidAttribute;
+ stmt.params['anno3'] = this._obsoleteWeaveAttribute;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+tests.push({
+ name: "A.3",
+ desc: "Remove unused attributes",
+
+ _usedPageAttribute: "usedPage",
+ _usedItemAttribute: "usedItem",
+ _unusedAttribute: "unused",
+ _placeId: null,
+ _bookmarkId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // add a bookmark
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add a used attribute and an unused one.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.reset();
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.reset();
+ stmt.params['anno'] = this._unusedAttribute;
+ stmt.execute();
+ stmt.finalize();
+
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attributes are still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that unused attribute has been removed
+ stmt.params['anno'] = this._unusedAttribute;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "B.1",
+ desc: "Remove annotations with an invalid attribute",
+
+ _usedPageAttribute: "usedPage",
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a used attribute.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ // Add an annotation with a nonexistent attribute
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, 1337)");
+ stmt.params['place_id'] = this._placeId;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that annotation with bogus attribute has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = 1337");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "B.2",
+ desc: "Remove orphan page annotations",
+
+ _usedPageAttribute: "usedPage",
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a used attribute.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_annos (place_id, anno_attribute_id) VALUES(:place_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['place_id'] = this._placeId;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.reset();
+ // Add an annotation to a nonexistent page
+ stmt.params['place_id'] = 1337;
+ stmt.params['anno'] = this._usedPageAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedPageAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that an annotation to a nonexistent page has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_annos WHERE place_id = 1337");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+tests.push({
+ name: "C.1",
+ desc: "fix missing Places root",
+
+ setup: function() {
+ // Sanity check: ensure that roots are intact.
+ do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0);
+ do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
+
+ // Remove the root.
+ mDBConn.executeSimpleSQL("DELETE FROM moz_bookmarks WHERE parent = 0");
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE parent = 0");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Ensure the roots have been correctly restored.
+ do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0);
+ do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
+ }
+});
+
+// ------------------------------------------------------------------------------
+tests.push({
+ name: "C.2",
+ desc: "Fix roots titles",
+
+ setup: function() {
+ // Sanity check: ensure that roots titles are correct. We can use our check.
+ this.check();
+ // Change some roots' titles.
+ bs.setItemTitle(bs.placesRoot, "bad title");
+ do_check_eq(bs.getItemTitle(bs.placesRoot), "bad title");
+ bs.setItemTitle(bs.unfiledBookmarksFolder, "bad title");
+ do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder), "bad title");
+ },
+
+ check: function() {
+ // Ensure all roots titles are correct.
+ do_check_eq(bs.getItemTitle(bs.placesRoot), "");
+ do_check_eq(bs.getItemTitle(bs.bookmarksMenuFolder),
+ PlacesUtils.getString("BookmarksMenuFolderTitle"));
+ do_check_eq(bs.getItemTitle(bs.tagsFolder),
+ PlacesUtils.getString("TagsFolderTitle"));
+ do_check_eq(bs.getItemTitle(bs.unfiledBookmarksFolder),
+ PlacesUtils.getString("OtherBookmarksFolderTitle"));
+ do_check_eq(bs.getItemTitle(bs.toolbarFolder),
+ PlacesUtils.getString("BookmarksToolbarFolderTitle"));
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.1",
+ desc: "Remove items without a valid place",
+
+ _validItemId: null,
+ _invalidItemId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this.placeId = addPlace();
+ // Insert a valid bookmark
+ this._validItemId = addBookmark(this.placeId);
+ // Insert a bookmark with an invalid place
+ this._invalidItemId = addBookmark(1337);
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id");
+ stmt.params["item_id"] = this._validItemId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that invalid bookmark has been removed
+ stmt.params["item_id"] = this._invalidItemId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.2",
+ desc: "Remove items that are not uri bookmarks from tag containers",
+
+ _tagId: null,
+ _bookmarkId: null,
+ _separatorId: null,
+ _folderId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Create a tag
+ this._tagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder);
+ // Insert a bookmark in the tag
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._tagId);
+ // Insert a separator in the tag
+ this._separatorId = addBookmark(null, bs.TYPE_SEPARATOR, this._tagId);
+ // Insert a folder in the tag
+ this._folderId = addBookmark(null, bs.TYPE_FOLDER, this._tagId);
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE type = :type AND parent = :parent");
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ stmt.params["parent"] = this._tagId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that separator is no more there
+ stmt.params["type"] = bs.TYPE_SEPARATOR;
+ stmt.params["parent"] = this._tagId;
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ // Check that folder is no more there
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ stmt.params["parent"] = this._tagId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.3",
+ desc: "Remove empty tags",
+
+ _tagId: null,
+ _bookmarkId: null,
+ _emptyTagId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Create a tag
+ this._tagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder);
+ // Insert a bookmark in the tag
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._tagId);
+ // Create another tag (empty)
+ this._emptyTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder);
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :id AND type = :type AND parent = :parent");
+ stmt.params["id"] = this._bookmarkId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ stmt.params["parent"] = this._tagId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["id"] = this._tagId;
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ stmt.params["parent"] = bs.tagsFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["id"] = this._emptyTagId;
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ stmt.params["parent"] = bs.tagsFolder;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.4",
+ desc: "Move orphan items to unsorted folder",
+
+ _orphanBookmarkId: null,
+ _orphanSeparatorId: null,
+ _orphanFolderId: null,
+ _bookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert an orphan bookmark
+ this._orphanBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, 8888);
+ // Insert an orphan separator
+ this._orphanSeparatorId = addBookmark(null, bs.TYPE_SEPARATOR, 8888);
+ // Insert a orphan folder
+ this._orphanFolderId = addBookmark(null, bs.TYPE_FOLDER, 8888);
+ // Create a child of the last created folder
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._orphanFolderId);
+ },
+
+ check: function() {
+ // Check that bookmarks are now children of a real folder (unsorted)
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent");
+ stmt.params["item_id"] = this._orphanBookmarkId;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._orphanSeparatorId;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._orphanFolderId;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._bookmarkId;
+ stmt.params["parent"] = this._orphanFolderId;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.6",
+ desc: "Fix wrong item types | bookmarks",
+
+ _separatorId: null,
+ _folderId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a separator with a fk
+ this._separatorId = addBookmark(this._placeId, bs.TYPE_SEPARATOR);
+ // Add a folder with a fk
+ this._folderId = addBookmark(this._placeId, bs.TYPE_FOLDER);
+ },
+
+ check: function() {
+ // Check that items with an fk have been converted to bookmarks
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND type = :type");
+ stmt.params["item_id"] = this._separatorId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._folderId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.7",
+ desc: "Fix wrong item types | bookmarks",
+
+ _validBookmarkId: null,
+ _invalidBookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a bookmark with a valid place id
+ this._validBookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK);
+ // Add a bookmark with a null place id
+ this._invalidBookmarkId = addBookmark(null, bs.TYPE_BOOKMARK);
+ },
+
+ check: function() {
+ // Check valid bookmark
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND type = :type");
+ stmt.params["item_id"] = this._validBookmarkId;
+ stmt.params["type"] = bs.TYPE_BOOKMARK;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check invalid bookmark has been converted to a folder
+ stmt.params["item_id"] = this._invalidBookmarkId;
+ stmt.params["type"] = bs.TYPE_FOLDER;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.9",
+ desc: "Fix wrong parents",
+
+ _bookmarkId: null,
+ _separatorId: null,
+ _bookmarkId1: null,
+ _bookmarkId2: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert a bookmark
+ this._bookmarkId = addBookmark(this._placeId, bs.TYPE_BOOKMARK);
+ // Insert a separator
+ this._separatorId = addBookmark(null, bs.TYPE_SEPARATOR);
+ // Create 3 children of these items
+ this._bookmarkId1 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._bookmarkId);
+ this._bookmarkId2 = addBookmark(this._placeId, bs.TYPE_BOOKMARK, this._separatorId);
+ },
+
+ check: function() {
+ // Check that bookmarks are now children of a real folder (unsorted)
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_bookmarks WHERE id = :item_id AND parent = :parent");
+ stmt.params["item_id"] = this._bookmarkId1;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["item_id"] = this._bookmarkId2;
+ stmt.params["parent"] = bs.unfiledBookmarksFolder;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.10",
+ desc: "Recalculate positions",
+
+ _unfiledBookmarks: [],
+ _toolbarBookmarks: [],
+
+ setup: function() {
+ const NUM_BOOKMARKS = 20;
+ bs.runInBatchMode({
+ runBatched: function (aUserData) {
+ // Add bookmarks to two folders to better perturbate the table.
+ for (let i = 0; i < NUM_BOOKMARKS; i++) {
+ bs.insertBookmark(PlacesUtils.unfiledBookmarksFolderId,
+ NetUtil.newURI("http://example.com/"),
+ bs.DEFAULT_INDEX, "testbookmark");
+ }
+ for (let i = 0; i < NUM_BOOKMARKS; i++) {
+ bs.insertBookmark(PlacesUtils.toolbarFolderId,
+ NetUtil.newURI("http://example.com/"),
+ bs.DEFAULT_INDEX, "testbookmark");
+ }
+ }
+ }, null);
+
+ function randomize_positions(aParent, aResultArray) {
+ let stmt = mDBConn.createStatement(
+ `UPDATE moz_bookmarks SET position = :rand
+ WHERE id IN (
+ SELECT id FROM moz_bookmarks WHERE parent = :parent
+ ORDER BY RANDOM() LIMIT 1
+ )`
+ );
+ for (let i = 0; i < (NUM_BOOKMARKS / 2); i++) {
+ stmt.params["parent"] = aParent;
+ stmt.params["rand"] = Math.round(Math.random() * (NUM_BOOKMARKS - 1));
+ stmt.execute();
+ stmt.reset();
+ }
+ stmt.finalize();
+
+ // Build the expected ordered list of bookmarks.
+ stmt = mDBConn.createStatement(
+ `SELECT id, position
+ FROM moz_bookmarks WHERE parent = :parent
+ ORDER BY position ASC, ROWID ASC`
+ );
+ stmt.params["parent"] = aParent;
+ while (stmt.executeStep()) {
+ aResultArray.push(stmt.row.id);
+ print(stmt.row.id + "\t" + stmt.row.position + "\t" +
+ (aResultArray.length - 1));
+ }
+ stmt.finalize();
+ }
+
+ // Set random positions for the added bookmarks.
+ randomize_positions(PlacesUtils.unfiledBookmarksFolderId,
+ this._unfiledBookmarks);
+ randomize_positions(PlacesUtils.toolbarFolderId, this._toolbarBookmarks);
+ },
+
+ check: function() {
+ function check_order(aParent, aResultArray) {
+ // Build the expected ordered list of bookmarks.
+ let stmt = mDBConn.createStatement(
+ `SELECT id, position FROM moz_bookmarks WHERE parent = :parent
+ ORDER BY position ASC`
+ );
+ stmt.params["parent"] = aParent;
+ let pass = true;
+ while (stmt.executeStep()) {
+ print(stmt.row.id + "\t" + stmt.row.position);
+ if (aResultArray.indexOf(stmt.row.id) != stmt.row.position) {
+ pass = false;
+ }
+ }
+ stmt.finalize();
+ if (!pass) {
+ dump_table("moz_bookmarks");
+ do_throw("Unexpected unfiled bookmarks order.");
+ }
+ }
+
+ check_order(PlacesUtils.unfiledBookmarksFolderId, this._unfiledBookmarks);
+ check_order(PlacesUtils.toolbarFolderId, this._toolbarBookmarks);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "D.12",
+ desc: "Fix empty-named tags",
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ let placeId = addPlace();
+ // Create a empty-named tag.
+ this._untitledTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder, null, null, "");
+ // Insert a bookmark in the tag, otherwise it will be removed.
+ addBookmark(placeId, bs.TYPE_BOOKMARK, this._untitledTagId);
+ // Create a empty-named folder.
+ this._untitledFolderId = addBookmark(null, bs.TYPE_FOLDER, bs.toolbarFolder, null, null, "");
+ // Create a titled tag.
+ this._titledTagId = addBookmark(null, bs.TYPE_FOLDER, bs.tagsFolder, null, null, "titledTag");
+ // Insert a bookmark in the tag, otherwise it will be removed.
+ addBookmark(placeId, bs.TYPE_BOOKMARK, this._titledTagId);
+ // Create a titled folder.
+ this._titledFolderId = addBookmark(null, bs.TYPE_FOLDER, bs.toolbarFolder, null, null, "titledFolder");
+ },
+
+ check: function() {
+ // Check that valid bookmark is still there
+ let stmt = mDBConn.createStatement(
+ "SELECT title FROM moz_bookmarks WHERE id = :id"
+ );
+ stmt.params["id"] = this._untitledTagId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "(notitle)");
+ stmt.reset();
+ stmt.params["id"] = this._untitledFolderId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "");
+ stmt.reset();
+ stmt.params["id"] = this._titledTagId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "titledTag");
+ stmt.reset();
+ stmt.params["id"] = this._titledFolderId;
+ do_check_true(stmt.executeStep());
+ do_check_eq(stmt.row.title, "titledFolder");
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "E.1",
+ desc: "Remove orphan icons",
+
+ _placeId: null,
+
+ setup: function() {
+ // Insert favicon entries
+ let stmt = mDBConn.createStatement("INSERT INTO moz_favicons (id, url) VALUES(:favicon_id, :url)");
+ stmt.params["favicon_id"] = 1;
+ stmt.params["url"] = "http://www1.mozilla.org/favicon.ico";
+ stmt.execute();
+ stmt.reset();
+ stmt.params["favicon_id"] = 2;
+ stmt.params["url"] = "http://www2.mozilla.org/favicon.ico";
+ stmt.execute();
+ stmt.finalize();
+ // Insert a place using the existing favicon entry
+ this._placeId = addPlace("http://www.mozilla.org", 1);
+ },
+
+ check: function() {
+ // Check that used icon is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_favicons WHERE id = :favicon_id");
+ stmt.params["favicon_id"] = 1;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that unused icon has been removed
+ stmt.params["favicon_id"] = 2;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "F.1",
+ desc: "Remove orphan visits",
+
+ _placeId: null,
+ _invalidPlaceId: 1337,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add a valid visit and an invalid one
+ stmt = mDBConn.createStatement("INSERT INTO moz_historyvisits(place_id) VALUES (:place_id)");
+ stmt.params["place_id"] = this._placeId;
+ stmt.execute();
+ stmt.reset();
+ stmt.params["place_id"] = this._invalidPlaceId;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that valid visit is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_historyvisits WHERE place_id = :place_id");
+ stmt.params["place_id"] = this._placeId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that invalid visit has been removed
+ stmt.params["place_id"] = this._invalidPlaceId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "G.1",
+ desc: "Remove orphan input history",
+
+ _placeId: null,
+ _invalidPlaceId: 1337,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Add input history entries
+ let stmt = mDBConn.createStatement("INSERT INTO moz_inputhistory (place_id, input) VALUES (:place_id, :input)");
+ stmt.params["place_id"] = this._placeId;
+ stmt.params["input"] = "moz";
+ stmt.execute();
+ stmt.reset();
+ stmt.params["place_id"] = this._invalidPlaceId;
+ stmt.params["input"] = "moz";
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that inputhistory on valid place is still there
+ let stmt = mDBConn.createStatement("SELECT place_id FROM moz_inputhistory WHERE place_id = :place_id");
+ stmt.params["place_id"] = this._placeId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ // Check that inputhistory on invalid place has gone
+ stmt.params["place_id"] = this._invalidPlaceId;
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "H.1",
+ desc: "Remove item annos with an invalid attribute",
+
+ _usedItemAttribute: "usedItem",
+ _bookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert a bookmark
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add a used attribute.
+ let stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ // Add an annotation with a nonexistent attribute
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES(:item_id, 1337)");
+ stmt.params['item_id'] = this._bookmarkId;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that annotation with bogus attribute has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = 1337");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "H.2",
+ desc: "Remove orphan item annotations",
+
+ _usedItemAttribute: "usedItem",
+ _bookmarkId: null,
+ _invalidBookmarkId: 8888,
+ _placeId: null,
+
+ setup: function() {
+ // Add a place to ensure place_id = 1 is valid
+ this._placeId = addPlace();
+ // Insert a bookmark
+ this._bookmarkId = addBookmark(this._placeId);
+ // Add a used attribute.
+ stmt = mDBConn.createStatement("INSERT INTO moz_anno_attributes (name) VALUES (:anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ stmt = mDBConn.createStatement("INSERT INTO moz_items_annos (item_id, anno_attribute_id) VALUES (:item_id, (SELECT id FROM moz_anno_attributes WHERE name = :anno))");
+ stmt.params["item_id"] = this._bookmarkId;
+ stmt.params["anno"] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.reset();
+ // Add an annotation to a nonexistent item
+ stmt.params["item_id"] = this._invalidBookmarkId;
+ stmt.params["anno"] = this._usedItemAttribute;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that used attribute is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_anno_attributes WHERE name = :anno");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // check that annotation with valid attribute is still there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE anno_attribute_id = (SELECT id FROM moz_anno_attributes WHERE name = :anno)");
+ stmt.params['anno'] = this._usedItemAttribute;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that an annotation to a nonexistent page has been removed
+ stmt = mDBConn.createStatement("SELECT id FROM moz_items_annos WHERE item_id = 8888");
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "I.1",
+ desc: "Remove unused keywords",
+
+ _bookmarkId: null,
+ _placeId: null,
+
+ setup: function() {
+ // Insert 2 keywords
+ let stmt = mDBConn.createStatement("INSERT INTO moz_keywords (id, keyword, place_id) VALUES(:id, :keyword, :place_id)");
+ stmt.params["id"] = 1;
+ stmt.params["keyword"] = "unused";
+ stmt.params["place_id"] = 100;
+ stmt.execute();
+ stmt.finalize();
+ },
+
+ check: function() {
+ // Check that "used" keyword is still there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_keywords WHERE keyword = :keyword");
+ // Check that "unused" keyword has gone
+ stmt.params["keyword"] = "unused";
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.1",
+ desc: "Fix wrong favicon ids",
+
+ _validIconPlaceId: null,
+ _invalidIconPlaceId: null,
+
+ setup: function() {
+ // Insert a favicon entry
+ let stmt = mDBConn.createStatement("INSERT INTO moz_favicons (id, url) VALUES(1, :url)");
+ stmt.params["url"] = "http://www.mozilla.org/favicon.ico";
+ stmt.execute();
+ stmt.finalize();
+ // Insert a place using the existing favicon entry
+ this._validIconPlaceId = addPlace("http://www1.mozilla.org", 1);
+
+ // Insert a place using a nonexistent favicon entry
+ this._invalidIconPlaceId = addPlace("http://www2.mozilla.org", 1337);
+ },
+
+ check: function() {
+ // Check that bogus favicon is not there
+ let stmt = mDBConn.createStatement("SELECT id FROM moz_places WHERE favicon_id = :favicon_id");
+ stmt.params["favicon_id"] = 1337;
+ do_check_false(stmt.executeStep());
+ stmt.reset();
+ // Check that valid favicon is still there
+ stmt.params["favicon_id"] = 1;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ // Check that place entries are there
+ stmt = mDBConn.createStatement("SELECT id FROM moz_places WHERE id = :place_id");
+ stmt.params["place_id"] = this._validIconPlaceId;
+ do_check_true(stmt.executeStep());
+ stmt.reset();
+ stmt.params["place_id"] = this._invalidIconPlaceId;
+ do_check_true(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.2",
+ desc: "Recalculate visit_count and last_visit_date",
+
+ setup: function* () {
+ function setVisitCount(aURL, aValue) {
+ let stmt = mDBConn.createStatement(
+ `UPDATE moz_places SET visit_count = :count WHERE url_hash = hash(:url)
+ AND url = :url`
+ );
+ stmt.params.count = aValue;
+ stmt.params.url = aURL;
+ stmt.execute();
+ stmt.finalize();
+ }
+ function setLastVisitDate(aURL, aValue) {
+ let stmt = mDBConn.createStatement(
+ `UPDATE moz_places SET last_visit_date = :date WHERE url_hash = hash(:url)
+ AND url = :url`
+ );
+ stmt.params.date = aValue;
+ stmt.params.url = aURL;
+ stmt.execute();
+ stmt.finalize();
+ }
+
+ let now = Date.now() * 1000;
+ // Add a page with 1 visit.
+ let url = "http://1.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ // Add a page with 1 visit and set wrong visit_count.
+ url = "http://2.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ setVisitCount(url, 10);
+ // Add a page with 1 visit and set wrong last_visit_date.
+ url = "http://3.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ setLastVisitDate(url, now++);
+ // Add a page with 1 visit and set wrong stats.
+ url = "http://4.moz.org/";
+ yield PlacesTestUtils.addVisits({ uri: uri(url), visitDate: now++ });
+ setVisitCount(url, 10);
+ setLastVisitDate(url, now++);
+
+ // Add a page without visits.
+ url = "http://5.moz.org/";
+ addPlace(url);
+ // Add a page without visits and set wrong visit_count.
+ url = "http://6.moz.org/";
+ addPlace(url);
+ setVisitCount(url, 10);
+ // Add a page without visits and set wrong last_visit_date.
+ url = "http://7.moz.org/";
+ addPlace(url);
+ setLastVisitDate(url, now++);
+ // Add a page without visits and set wrong stats.
+ url = "http://8.moz.org/";
+ addPlace(url);
+ setVisitCount(url, 10);
+ setLastVisitDate(url, now++);
+ },
+
+ check: function() {
+ let stmt = mDBConn.createStatement(
+ `SELECT h.id FROM moz_places h
+ JOIN moz_historyvisits v ON v.place_id = h.id AND visit_type NOT IN (0,4,7,8,9)
+ GROUP BY h.id HAVING h.visit_count <> count(*)
+ UNION ALL
+ SELECT h.id FROM moz_places h
+ JOIN moz_historyvisits v ON v.place_id = h.id
+ GROUP BY h.id HAVING h.last_visit_date <> MAX(v.visit_date)`
+ );
+ do_check_false(stmt.executeStep());
+ stmt.finalize();
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.3",
+ desc: "recalculate hidden for redirects.",
+
+ *setup() {
+ yield PlacesTestUtils.addVisits([
+ { uri: NetUtil.newURI("http://l3.moz.org/"),
+ transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://l3.moz.org/redirecting/"),
+ transition: TRANSITION_TYPED },
+ { uri: NetUtil.newURI("http://l3.moz.org/redirecting2/"),
+ transition: TRANSITION_REDIRECT_TEMPORARY,
+ referrer: NetUtil.newURI("http://l3.moz.org/redirecting/") },
+ { uri: NetUtil.newURI("http://l3.moz.org/target/"),
+ transition: TRANSITION_REDIRECT_PERMANENT,
+ referrer: NetUtil.newURI("http://l3.moz.org/redirecting2/") },
+ ]);
+ },
+
+ check: function () {
+ return new Promise(resolve => {
+ let stmt = mDBConn.createAsyncStatement(
+ "SELECT h.url FROM moz_places h WHERE h.hidden = 1"
+ );
+ stmt.executeAsync({
+ _count: 0,
+ handleResult: function(aResultSet) {
+ for (let row; (row = aResultSet.getNextRow());) {
+ let url = row.getResultByIndex(0);
+ do_check_true(/redirecting/.test(url));
+ this._count++;
+ }
+ },
+ handleError: function(aError) {
+ },
+ handleCompletion: function(aReason) {
+ dump_table("moz_places");
+ dump_table("moz_historyvisits");
+ do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED);
+ do_check_eq(this._count, 2);
+ resolve();
+ }
+ });
+ stmt.finalize();
+ });
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.4",
+ desc: "recalculate foreign_count.",
+
+ *setup() {
+ this._pageGuid = (yield PlacesUtils.history.insert({ url: "http://l4.moz.org/",
+ visits: [{ date: new Date() }] })).guid;
+ yield PlacesUtils.bookmarks.insert({ url: "http://l4.moz.org/",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid});
+ yield PlacesUtils.keywords.insert({ url: "http://l4.moz.org/", keyword: "kw" });
+ Assert.equal((yield this._getForeignCount()), 2);
+ },
+
+ *_getForeignCount() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT foreign_count FROM moz_places
+ WHERE guid = :guid`, { guid: this._pageGuid });
+ return rows[0].getResultByName("foreign_count");
+ },
+
+ *check() {
+ Assert.equal((yield this._getForeignCount()), 2);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "L.5",
+ desc: "recalculate hashes when missing.",
+
+ *setup() {
+ this._pageGuid = (yield PlacesUtils.history.insert({ url: "http://l5.moz.org/",
+ visits: [{ date: new Date() }] })).guid;
+ Assert.ok((yield this._getHash()) > 0);
+ yield PlacesUtils.withConnectionWrapper("change url hash", Task.async(function* (db) {
+ yield db.execute(`UPDATE moz_places SET url_hash = 0`);
+ }));
+ Assert.equal((yield this._getHash()), 0);
+ },
+
+ *_getHash() {
+ let db = yield PlacesUtils.promiseDBConnection();
+ let rows = yield db.execute(`SELECT url_hash FROM moz_places
+ WHERE guid = :guid`, { guid: this._pageGuid });
+ return rows[0].getResultByName("url_hash");
+ },
+
+ *check() {
+ Assert.ok((yield this._getHash()) > 0);
+ }
+});
+
+// ------------------------------------------------------------------------------
+
+tests.push({
+ name: "Z",
+ desc: "Sanity: Preventive maintenance does not touch valid items",
+
+ _uri1: uri("http://www1.mozilla.org"),
+ _uri2: uri("http://www2.mozilla.org"),
+ _folderId: null,
+ _bookmarkId: null,
+ _separatorId: null,
+
+ setup: function* () {
+ // use valid api calls to create a bunch of items
+ yield PlacesTestUtils.addVisits([
+ { uri: this._uri1 },
+ { uri: this._uri2 },
+ ]);
+
+ this._folderId = bs.createFolder(bs.toolbarFolder, "testfolder",
+ bs.DEFAULT_INDEX);
+ do_check_true(this._folderId > 0);
+ this._bookmarkId = bs.insertBookmark(this._folderId, this._uri1,
+ bs.DEFAULT_INDEX, "testbookmark");
+ do_check_true(this._bookmarkId > 0);
+ this._separatorId = bs.insertSeparator(bs.unfiledBookmarksFolder,
+ bs.DEFAULT_INDEX);
+ do_check_true(this._separatorId > 0);
+ ts.tagURI(this._uri1, ["testtag"]);
+ fs.setAndFetchFaviconForPage(this._uri2, SMALLPNG_DATA_URI, false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ null,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ yield PlacesUtils.keywords.insert({ url: this._uri1.spec, keyword: "testkeyword" });
+ as.setPageAnnotation(this._uri2, "anno", "anno", 0, as.EXPIRE_NEVER);
+ as.setItemAnnotation(this._bookmarkId, "anno", "anno", 0, as.EXPIRE_NEVER);
+ },
+
+ check: Task.async(function* () {
+ // Check that all items are correct
+ let isVisited = yield promiseIsURIVisited(this._uri1);
+ do_check_true(isVisited);
+ isVisited = yield promiseIsURIVisited(this._uri2);
+ do_check_true(isVisited);
+
+ do_check_eq(bs.getBookmarkURI(this._bookmarkId).spec, this._uri1.spec);
+ do_check_eq(bs.getItemIndex(this._folderId), 0);
+ do_check_eq(bs.getItemType(this._folderId), bs.TYPE_FOLDER);
+ do_check_eq(bs.getItemType(this._separatorId), bs.TYPE_SEPARATOR);
+
+ do_check_eq(ts.getTagsForURI(this._uri1).length, 1);
+ do_check_eq((yield PlacesUtils.keywords.fetch({ url: this._uri1.spec })).keyword, "testkeyword");
+ do_check_eq(as.getPageAnnotation(this._uri2, "anno"), "anno");
+ do_check_eq(as.getItemAnnotation(this._bookmarkId, "anno"), "anno");
+
+ yield new Promise(resolve => {
+ fs.getFaviconURLForPage(this._uri2, aFaviconURI => {
+ do_check_true(aFaviconURI.equals(SMALLPNG_DATA_URI));
+ resolve();
+ });
+ });
+ })
+});
+
+// ------------------------------------------------------------------------------
+
+add_task(function* test_preventive_maintenance()
+{
+ // Get current bookmarks max ID for cleanup
+ let stmt = mDBConn.createStatement("SELECT MAX(id) FROM moz_bookmarks");
+ stmt.executeStep();
+ defaultBookmarksMaxId = stmt.getInt32(0);
+ stmt.finalize();
+ do_check_true(defaultBookmarksMaxId > 0);
+
+ for (let test of tests) {
+ dump("\nExecuting test: " + test.name + "\n" + "*** " + test.desc + "\n");
+ yield test.setup();
+
+ let promiseMaintenanceFinished =
+ promiseTopicObserved(FINISHED_MAINTENANCE_NOTIFICATION_TOPIC);
+ Services.prefs.clearUserPref("places.database.lastMaintenance");
+ let callbackInvoked = false;
+ PlacesDBUtils.maintenanceOnIdle(() => callbackInvoked = true);
+ yield promiseMaintenanceFinished;
+ do_check_true(callbackInvoked);
+
+ // Check the lastMaintenance time has been saved.
+ do_check_neq(Services.prefs.getIntPref("places.database.lastMaintenance"), null);
+
+ yield test.check();
+
+ cleanDatabase();
+ }
+
+ // Sanity check: all roots should be intact
+ do_check_eq(bs.getFolderIdForItem(bs.placesRoot), 0);
+ do_check_eq(bs.getFolderIdForItem(bs.bookmarksMenuFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.tagsFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.unfiledBookmarksFolder), bs.placesRoot);
+ do_check_eq(bs.getFolderIdForItem(bs.toolbarFolder), bs.placesRoot);
+});
diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js
new file mode 100644
index 000000000..a8acb4be0
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance_checkAndFixDatabase.js
@@ -0,0 +1,50 @@
+/* -*- 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 preventive maintenance checkAndFixDatabase.
+ */
+
+// Include PlacesDBUtils module.
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+function run_test() {
+ do_test_pending();
+ PlacesDBUtils.checkAndFixDatabase(function(aLog) {
+ let sections = [];
+ let positives = [];
+ let negatives = [];
+ let infos = [];
+
+ aLog.forEach(function (aMsg) {
+ print (aMsg);
+ switch (aMsg.substr(0, 1)) {
+ case "+":
+ positives.push(aMsg);
+ break;
+ case "-":
+ negatives.push(aMsg);
+ break;
+ case ">":
+ sections.push(aMsg);
+ break;
+ default:
+ infos.push(aMsg);
+ }
+ });
+
+ print("Check that we have run all sections.");
+ do_check_eq(sections.length, 5);
+ print("Check that we have no negatives.");
+ do_check_false(!!negatives.length);
+ print("Check that we have positives.");
+ do_check_true(!!positives.length);
+ print("Check that we have info.");
+ do_check_true(!!infos.length);
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js b/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js
new file mode 100644
index 000000000..ebe308f03
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_preventive_maintenance_runTasks.js
@@ -0,0 +1,46 @@
+/* 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 preventive maintenance runTasks.
+ */
+
+// Include PlacesDBUtils module.
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+function run_test() {
+ do_test_pending();
+ PlacesDBUtils.runTasks([PlacesDBUtils.reindex], function(aLog) {
+ let sections = [];
+ let positives = [];
+ let negatives = [];
+ let infos = [];
+
+ aLog.forEach(function (aMsg) {
+ print (aMsg);
+ switch (aMsg.substr(0, 1)) {
+ case "+":
+ positives.push(aMsg);
+ break;
+ case "-":
+ negatives.push(aMsg);
+ break;
+ case ">":
+ sections.push(aMsg);
+ break;
+ default:
+ infos.push(aMsg);
+ }
+ });
+
+ print("Check that we have run all sections.");
+ do_check_eq(sections.length, 1);
+ print("Check that we have no negatives.");
+ do_check_false(!!negatives.length);
+ print("Check that we have positives.");
+ do_check_true(!!positives.length);
+
+ do_test_finished();
+ });
+}
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 000000000..0719a0cd4
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_promiseBookmarksTree.js
@@ -0,0 +1,256 @@
+/* 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* check_has_child(aParentGuid, aChildGuid) {
+ let parentTree = yield PlacesUtils.promiseBookmarksTree(aParentGuid);
+ do_check_true("children" in parentTree);
+ do_check_true(parentTree.children.find( e => e.guid == aChildGuid ) != null);
+}
+
+function* compareToNode(aItem, aNode, aIsRootItem, aExcludedGuids = []) {
+ // itemId==-1 indicates a non-bookmark node, which is unexpected.
+ do_check_neq(aNode.itemId, -1);
+
+ function check_unset(...aProps) {
+ aProps.forEach( p => { do_check_false(p in aItem); } );
+ }
+ function strict_eq_check(v1, v2) {
+ dump("v1: " + v1 + " v2: " + v2 + "\n");
+ do_check_eq(typeof v1, typeof v2);
+ do_check_eq(v1, v2);
+ }
+ function compare_prop(aItemProp, aNodeProp = aItemProp, aOptional = false) {
+ if (aOptional && aNode[aNodeProp] === null)
+ check_unset(aItemProp);
+ else
+ strict_eq_check(aItem[aItemProp], aNode[aNodeProp]);
+ }
+ function compare_prop_to_value(aItemProp, aValue, aOptional = true) {
+ if (aOptional && aValue === null)
+ check_unset(aItemProp);
+ else
+ strict_eq_check(aItem[aItemProp], aValue);
+ }
+
+ // Bug 1013053 - bookmarkIndex is unavailable for the query's root
+ if (aNode.bookmarkIndex == -1) {
+ compare_prop_to_value("index",
+ PlacesUtils.bookmarks.getItemIndex(aNode.itemId),
+ false);
+ }
+ else {
+ compare_prop("index", "bookmarkIndex");
+ }
+
+ compare_prop("dateAdded");
+ compare_prop("lastModified");
+
+ if (aIsRootItem && aNode.itemId != PlacesUtils.placesRootId) {
+ do_check_true("parentGuid" in aItem);
+ yield check_has_child(aItem.parentGuid, aItem.guid)
+ }
+ else {
+ check_unset("parentGuid");
+ }
+
+ let expectedAnnos = PlacesUtils.getAnnotationsForItem(aItem.id);
+ if (expectedAnnos.length > 0) {
+ let annosToString = annos => {
+ return annos.map(a => a.name + ":" + a.value).sort().join(",");
+ };
+ do_check_true(Array.isArray(aItem.annos))
+ do_check_eq(annosToString(aItem.annos), annosToString(expectedAnnos));
+ }
+ else {
+ check_unset("annos");
+ }
+ 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:
+ do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER);
+ 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 > 0) {
+ do_check_true(Array.isArray(aItem.children));
+ do_check_eq(aItem.children.length, expectedChildrenNodes.length);
+ for (let i = 0; i < aItem.children.length; i++) {
+ nodesCount +=
+ yield compareToNode(aItem.children[i], expectedChildrenNodes[i],
+ false, aExcludedGuids);
+ }
+ }
+ else {
+ check_unset("children");
+ }
+
+ switch (aItem.id) {
+ case PlacesUtils.placesRootId:
+ compare_prop_to_value("root", "placesRoot");
+ break;
+ case PlacesUtils.bookmarksMenuFolderId:
+ compare_prop_to_value("root", "bookmarksMenuFolder");
+ break;
+ case PlacesUtils.toolbarFolderId:
+ compare_prop_to_value("root", "toolbarFolder");
+ break;
+ case PlacesUtils.unfiledBookmarksFolderId:
+ compare_prop_to_value("root", "unfiledBookmarksFolder");
+ break;
+ case PlacesUtils.mobileFolderId:
+ compare_prop_to_value("root", "mobileFolder");
+ break;
+ default:
+ check_unset("root");
+ }
+ break;
+ case Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR:
+ do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR);
+ check_unset(...BOOKMARK_ONLY_PROPS, ...FOLDER_ONLY_PROPS);
+ break;
+ default:
+ do_check_eq(aItem.type, PlacesUtils.TYPE_X_MOZ_PLACE);
+ compare_prop("uri");
+ // node.tags's format is "a, b" whilst promiseBoookmarksTree is "a,b"
+ if (aNode.tags === null)
+ check_unset("tags");
+ else
+ compare_prop_to_value("tags", aNode.tags.replace(/, /g, ","), false);
+
+ if (aNode.icon) {
+ let nodeIconData = aNode.icon.replace("moz-anno:favicon:", "");
+ compare_prop_to_value("iconuri", nodeIconData);
+ }
+ else {
+ check_unset(aItem.iconuri);
+ }
+
+ check_unset(...FOLDER_ONLY_PROPS);
+
+ let itemURI = uri(aNode.uri);
+ compare_prop_to_value("charset",
+ yield PlacesUtils.getCharsetForURI(itemURI));
+
+ let entry = yield PlacesUtils.keywords.fetch({ url: aNode.uri });
+ compare_prop_to_value("keyword", entry ? entry.keyword : null);
+
+ if ("title" in aItem)
+ compare_prop("title");
+ else
+ do_check_null(aNode.title);
+ }
+
+ if (aIsRootItem)
+ do_check_eq(aItem.itemsCount, nodesCount);
+
+ return nodesCount;
+}
+
+var itemsCount = 0;
+function* new_bookmark(aInfo) {
+ ++itemsCount;
+ if (!("url" in aInfo))
+ aInfo.url = uri("http://test.item." + itemsCount);
+
+ if (!("title" in aInfo))
+ aInfo.title = "Test Item (bookmark) " + itemsCount;
+
+ yield PlacesTransactions.NewBookmark(aInfo).transact();
+}
+
+function* new_folder(aInfo) {
+ if (!("title" in aInfo))
+ aInfo.title = "Test Item (folder) " + itemsCount;
+ return yield 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).
+function* test_promiseBookmarksTreeForEachNode(aNode, aOptions, aExcludedGuids) {
+ do_check_true(aNode.bookmarkGuid && aNode.bookmarkGuid.length > 0);
+ let item = yield PlacesUtils.promiseBookmarksTree(aNode.bookmarkGuid, aOptions);
+ yield* compareToNode(item, aNode, true, aExcludedGuids);
+
+ for (let i = 0; i < aNode.childCount; i++) {
+ let child = aNode.getChild(i);
+ if (child.itemId != PlacesUtils.tagsFolderId)
+ yield test_promiseBookmarksTreeForEachNode(child,
+ { includeItemIds: true },
+ aExcludedGuids);
+ }
+ return item;
+}
+
+function* test_promiseBookmarksTreeAgainstResult(aItemGuid = "",
+ aOptions = { includeItemIds: true },
+ aExcludedGuids) {
+ let itemId = aItemGuid ?
+ yield PlacesUtils.promiseItemId(aItemGuid) : PlacesUtils.placesRootId;
+ let node = PlacesUtils.getFolderContents(itemId).root;
+ return yield test_promiseBookmarksTreeForEachNode(node, aOptions, aExcludedGuids);
+}
+
+add_task(function* () {
+ // Add some bookmarks to cover various use cases.
+ yield new_bookmark({ parentGuid: PlacesUtils.bookmarks.toolbarGuid });
+ yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ annotations: [{ name: "TestAnnoA", value: "TestVal" },
+ { name: "TestAnnoB", value: 0 }]});
+ let sepInfo = { parentGuid: PlacesUtils.bookmarks.menuGuid };
+ yield PlacesTransactions.NewSeparator(sepInfo).transact();
+ let folderGuid = yield new_folder({ parentGuid: PlacesUtils.bookmarks.menuGuid });
+ yield new_bookmark({ title: null,
+ parentGuid: folderGuid,
+ keyword: "test_keyword",
+ tags: ["TestTagA", "TestTagB"],
+ annotations: [{ name: "TestAnnoA", value: "TestVal2"}]});
+ let urlWithCharsetAndFavicon = uri("http://charset.and.favicon");
+ yield new_bookmark({ parentGuid: folderGuid, url: urlWithCharsetAndFavicon });
+ yield PlacesUtils.setCharsetForURI(urlWithCharsetAndFavicon, "UTF-8");
+ yield promiseSetIconForPage(urlWithCharsetAndFavicon, SMALLPNG_DATA_URI);
+ // Test the default places root without specifying it.
+ yield test_promiseBookmarksTreeAgainstResult();
+
+ // Do specify it
+ yield 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 =
+ yield test_promiseBookmarksTreeAgainstResult(PlacesUtils.bookmarks.rootGuid, {
+ excludeItemsCallback: aItem => {
+ guidsPassedToExcludeCallback.add(aItem.guid);
+ return aItem.root == "bookmarksMenuFolder";
+ },
+ includeItemIds: true
+ }, [PlacesUtils.bookmarks.menuGuid]);
+ do_check_eq(guidsPassedToExcludeCallback.size, 5);
+ do_check_eq(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 000000000..01fb3eef9
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_resolveNullBookmarkTitles.js
@@ -0,0 +1,49 @@
+/* -*- 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() {
+ run_next_test();
+}
+
+add_test(function test_resolveNullBookmarkTitles() {
+ let uri1 = uri("http://foo.tld/");
+ let uri2 = uri("https://bar.tld/");
+
+ PlacesTestUtils.addVisits([
+ { uri: uri1, title: "foo title" },
+ { uri: uri2, title: "bar title" }
+ ]).then(function () {
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri1,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ null);
+ PlacesUtils.bookmarks.insertBookmark(PlacesUtils.bookmarksMenuFolderId,
+ uri2,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ null);
+
+ PlacesUtils.tagging.tagURI(uri1, ["tag 1"]);
+ PlacesUtils.tagging.tagURI(uri2, ["tag 2"]);
+
+ let options = PlacesUtils.history.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ options.resultType = options.RESULTS_AS_TAG_CONTENTS;
+
+ let query = PlacesUtils.history.getNewQuery();
+ // if we don't set a tag folder, RESULTS_AS_TAG_CONTENTS will return all
+ // tagged URIs
+ let root = PlacesUtils.history.executeQuery(query, options).root;
+ root.containerOpen = true;
+ do_check_eq(root.childCount, 2);
+ // actually RESULTS_AS_TAG_CONTENTS return results ordered by place_id DESC
+ // so they are reversed
+ do_check_eq(root.getChild(0).title, "bar title");
+ do_check_eq(root.getChild(1).title, "foo title");
+ root.containerOpen = false;
+
+ run_next_test();
+ });
+});
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 000000000..35405ac50
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_result_sort.js
@@ -0,0 +1,139 @@
+/* -*- 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;
+
+/**
+ * Waits for onItemVisited notifications to be received.
+ */
+function promiseOnItemVisited() {
+ let defer = Promise.defer();
+ let bookmarksObserver = {
+ __proto__: NavBookmarkObserver.prototype,
+ onItemVisited: function BO_onItemVisited() {
+ PlacesUtils.bookmarks.removeObserver(this);
+ // Enqueue to be sure that all onItemVisited notifications ran.
+ do_execute_soon(defer.resolve);
+ }
+ };
+ PlacesUtils.bookmarks.addObserver(bookmarksObserver, false);
+ return defer.promise;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* test() {
+ let testFolder = PlacesUtils.bookmarks.createFolder(
+ PlacesUtils.bookmarks.placesRoot,
+ "Result-sort functionality tests root",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+
+ let uri1 = NetUtil.newURI("http://foo.tld/a");
+ let uri2 = NetUtil.newURI("http://foo.tld/b");
+
+ let id1 = PlacesUtils.bookmarks.insertBookmark(
+ testFolder, uri1, PlacesUtils.bookmarks.DEFAULT_INDEX, "b");
+ let id2 = PlacesUtils.bookmarks.insertBookmark(
+ testFolder, uri2, PlacesUtils.bookmarks.DEFAULT_INDEX, "a");
+ // url of id1, title of id2
+ let id3 = PlacesUtils.bookmarks.insertBookmark(
+ testFolder, uri1, PlacesUtils.bookmarks.DEFAULT_INDEX, "a");
+
+ // query with natural order
+ let result = PlacesUtils.getFolderContents(testFolder);
+ let root = result.root;
+
+ do_check_eq(root.childCount, 3);
+
+ function checkOrder(a, b, c) {
+ do_check_eq(root.getChild(0).itemId, a);
+ do_check_eq(root.getChild(1).itemId, b);
+ do_check_eq(root.getChild(2).itemId, c);
+ }
+
+ // natural order
+ do_print("Natural order");
+ checkOrder(id1, id2, id3);
+
+ // title: id3 should precede id2 since we fall-back to URI-based sorting
+ do_print("Sort by title asc");
+ result.sortingMode = NHQO.SORT_BY_TITLE_ASCENDING;
+ checkOrder(id3, id2, id1);
+
+ // In reverse
+ do_print("Sort by title desc");
+ result.sortingMode = NHQO.SORT_BY_TITLE_DESCENDING;
+ checkOrder(id1, id2, id3);
+
+ // uri sort: id1 should precede id3 since we fall-back to natural order
+ do_print("Sort by uri asc");
+ result.sortingMode = NHQO.SORT_BY_URI_ASCENDING;
+ checkOrder(id1, id3, id2);
+
+ // test live update
+ do_print("Change bookmark uri liveupdate");
+ PlacesUtils.bookmarks.changeBookmarkURI(id1, uri2);
+ checkOrder(id3, id1, id2);
+ PlacesUtils.bookmarks.changeBookmarkURI(id1, uri1);
+ checkOrder(id1, id3, id2);
+
+ // keyword sort
+ do_print("Sort by keyword asc");
+ result.sortingMode = NHQO.SORT_BY_KEYWORD_ASCENDING;
+ checkOrder(id3, id2, id1); // no keywords set - falling back to title sort
+ yield PlacesUtils.keywords.insert({ url: uri1.spec, keyword: "a" });
+ yield PlacesUtils.keywords.insert({ url: uri2.spec, keyword: "z" });
+ checkOrder(id3, id1, id2);
+
+ // XXXtodo: test history sortings (visit count, visit date)
+ // XXXtodo: test different item types once folderId and bookmarkId are merged.
+ // XXXtodo: test sortingAnnotation functionality with non-bookmark nodes
+
+ do_print("Sort by annotation desc");
+ PlacesUtils.annotations.setItemAnnotation(id1, "testAnno", "a", 0, 0);
+ PlacesUtils.annotations.setItemAnnotation(id3, "testAnno", "b", 0, 0);
+ result.sortingAnnotation = "testAnno";
+ result.sortingMode = NHQO.SORT_BY_ANNOTATION_DESCENDING;
+
+ // id1 precedes id2 per title-descending fallback
+ checkOrder(id3, id1, id2);
+
+ // XXXtodo: test dateAdded sort
+ // XXXtodo: test lastModified sort
+
+ // test live update
+ do_print("Annotation liveupdate");
+ PlacesUtils.annotations.setItemAnnotation(id1, "testAnno", "c", 0, 0);
+ checkOrder(id1, id3, id2);
+
+ // Add a visit, then check frecency ordering.
+
+ // When the bookmarks service gets onVisit, it asynchronously fetches all
+ // items for that visit, and then notifies onItemVisited. Thus we must
+ // explicitly wait for that.
+ let waitForVisited = promiseOnItemVisited();
+ yield PlacesTestUtils.addVisits({ uri: uri2, transition: TRANSITION_TYPED });
+ yield waitForVisited;
+
+ do_print("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 id1 and id3, since they have same frecency and no visits, fallback
+ // to sort by the newest bookmark.
+ checkOrder(id2, id3, id1);
+ do_print("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(id1, id3, id2);
+
+ 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 000000000..8e71ffd0d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_resultsAsVisit_details.js
@@ -0,0 +1,85 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+const {bookmarks, history} = PlacesUtils
+
+add_task(function* test_addVisitCheckFields() {
+ let uri = NetUtil.newURI("http://test4.com/");
+ yield 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");
+ equal(child.fromVisitId, -1, "Should have no referrer visit ID");
+
+ 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");
+ equal(child.fromVisitId, 1, "First visit should be the referring visit");
+
+ 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");
+ equal(child.fromVisitId, -1, "Should have no referrer visit ID");
+
+ 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");
+ equal(child.fromVisitId, -1, "Referrer visit id should be -1");
+
+ root.containerOpen = false;
+
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* test_bookmarkFields() {
+ let folder = bookmarks.createFolder(bookmarks.placesRoot, "test folder", bookmarks.DEFAULT_INDEX);
+ bookmarks.insertBookmark(folder, uri("http://test4.com/"),
+ bookmarks.DEFAULT_INDEX, "test4 title");
+
+ let root = PlacesUtils.getFolderContents(folder).root;
+ equal(root.childCount, 1);
+
+ equal(root.visitType, 0, "Visit type should be 0");
+ equal(root.visitId, -1, "Visit ID should be -1");
+ equal(root.fromVisitId, -1, "Referrer 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");
+ equal(child.fromVisitId, -1, "Referrer visit id should be -1");
+
+ root.containerOpen = false;
+
+ yield bookmarks.eraseEverything();
+});
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 000000000..41e6bab9e
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_sql_guid_functions.js
@@ -0,0 +1,106 @@
+/* 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)
+{
+ do_print("Checking guid '" + aGuid + "'");
+
+ do_check_valid_places_guid(aGuid);
+}
+
+// Test Functions
+
+function test_guid_invariants()
+{
+ const kExpectedChars = 64;
+ const kAllowedChars =
+ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_"
+ do_check_eq(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)) {
+ do_check_true(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++) {
+ do_check_true(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: function(aResult) {
+ try {
+ let row = aResult.getNextRow();
+ check_invariants(row.getResultByIndex(0));
+ do_check_eq(aResult.getNextRow(), null);
+ checked = true;
+ }
+ catch (e) {
+ do_throw(e);
+ }
+ },
+ handleCompletion: function(aReason) {
+ do_check_eq(aReason, Ci.mozIStorageStatementCallback.REASON_FINISHED);
+ do_check_true(checked);
+ run_next_test();
+ }
+ });
+ stmt.finalize();
+}
+
+// Test Runner
+
+[
+ test_guid_invariants,
+ test_guid_on_background,
+].forEach(add_test);
+
+function run_test()
+{
+ run_next_test();
+}
diff --git a/toolkit/components/places/tests/unit/test_svg_favicon.js b/toolkit/components/places/tests/unit/test_svg_favicon.js
new file mode 100644
index 000000000..cec40ddef
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_svg_favicon.js
@@ -0,0 +1,31 @@
+const PAGEURI = NetUtil.newURI("http://deliciousbacon.com/");
+
+add_task(function* () {
+ // First, add a history entry or else Places can't save a favicon.
+ yield PlacesTestUtils.addVisits({
+ uri: PAGEURI,
+ transition: TRANSITION_LINK,
+ visitDate: Date.now() * 1000
+ });
+
+ yield new Promise(resolve => {
+ function onSetComplete(aURI, aDataLen, aData, aMimeType) {
+ equal(aURI.spec, SMALLSVG_DATA_URI.spec, "setFavicon aURI check");
+ equal(aDataLen, 263, "setFavicon aDataLen check");
+ equal(aMimeType, "image/svg+xml", "setFavicon aMimeType check");
+ resolve();
+ }
+
+ PlacesUtils.favicons.setAndFetchFaviconForPage(PAGEURI, SMALLSVG_DATA_URI,
+ false,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ onSetComplete,
+ Services.scriptSecurityManager.getSystemPrincipal());
+ });
+
+ let data = yield PlacesUtils.promiseFaviconData(PAGEURI.spec);
+ equal(data.uri.spec, SMALLSVG_DATA_URI.spec, "getFavicon aURI check");
+ equal(data.dataLen, 263, "getFavicon aDataLen check");
+ equal(data.mimeType, "image/svg+xml", "getFavicon aMimeType check");
+});
+
diff --git a/toolkit/components/places/tests/unit/test_sync_utils.js b/toolkit/components/places/tests/unit/test_sync_utils.js
new file mode 100644
index 000000000..f8c7e6b58
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_sync_utils.js
@@ -0,0 +1,1150 @@
+Cu.import("resource://gre/modules/PlacesSyncUtils.jsm");
+Cu.import("resource://testing-common/httpd.js");
+Cu.importGlobalProperties(["crypto", "URLSearchParams"]);
+
+const DESCRIPTION_ANNO = "bookmarkProperties/description";
+const LOAD_IN_SIDEBAR_ANNO = "bookmarkProperties/loadInSidebar";
+const SYNC_PARENT_ANNO = "sync/parent";
+
+function makeGuid() {
+ return ChromeUtils.base64URLEncode(crypto.getRandomValues(new Uint8Array(9)), {
+ pad: false,
+ });
+}
+
+function makeLivemarkServer() {
+ let server = new HttpServer();
+ server.registerPrefixHandler("/feed/", do_get_file("./livemark.xml"));
+ server.start(-1);
+ return {
+ server,
+ get site() {
+ let { identity } = server;
+ let host = identity.primaryHost.includes(":") ?
+ `[${identity.primaryHost}]` : identity.primaryHost;
+ return `${identity.primaryScheme}://${host}:${identity.primaryPort}`;
+ },
+ stopServer() {
+ return new Promise(resolve => server.stop(resolve));
+ },
+ };
+}
+
+function shuffle(array) {
+ let results = [];
+ for (let i = 0; i < array.length; ++i) {
+ let randomIndex = Math.floor(Math.random() * (i + 1));
+ results[i] = results[randomIndex];
+ results[randomIndex] = array[i];
+ }
+ return results;
+}
+
+function compareAscending(a, b) {
+ if (a > b) {
+ return 1;
+ }
+ if (a < b) {
+ return -1;
+ }
+ return 0;
+}
+
+function assertTagForURLs(tag, urls, message) {
+ let taggedURLs = PlacesUtils.tagging.getURIsForTag(tag).map(uri => uri.spec);
+ deepEqual(taggedURLs.sort(compareAscending), urls.sort(compareAscending), message);
+}
+
+function assertURLHasTags(url, tags, message) {
+ let actualTags = PlacesUtils.tagging.getTagsForURI(uri(url));
+ deepEqual(actualTags.sort(compareAscending), tags, message);
+}
+
+var populateTree = Task.async(function* populate(parentGuid, ...items) {
+ let guids = {};
+
+ for (let index = 0; index < items.length; index++) {
+ let item = items[index];
+ let guid = makeGuid();
+
+ switch (item.kind) {
+ case "bookmark":
+ case "query":
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: item.url,
+ title: item.title,
+ parentGuid, guid, index,
+ });
+ break;
+
+ case "separator":
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid, guid,
+ });
+ break;
+
+ case "folder":
+ yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: item.title,
+ parentGuid, guid,
+ });
+ if (item.children) {
+ Object.assign(guids, yield* populate(guid, ...item.children));
+ }
+ break;
+
+ default:
+ throw new Error(`Unsupported item type: ${item.type}`);
+ }
+
+ if (item.exclude) {
+ let itemId = yield PlacesUtils.promiseItemId(guid);
+ PlacesUtils.annotations.setItemAnnotation(
+ itemId, PlacesUtils.EXCLUDE_FROM_BACKUP_ANNO, "Don't back this up", 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ }
+
+ guids[item.title] = guid;
+ }
+
+ return guids;
+});
+
+var syncIdToId = Task.async(function* syncIdToId(syncId) {
+ let guid = yield PlacesSyncUtils.bookmarks.syncIdToGuid(syncId);
+ return PlacesUtils.promiseItemId(guid);
+});
+
+add_task(function* test_order() {
+ do_print("Insert some bookmarks");
+ let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, {
+ kind: "bookmark",
+ title: "childBmk",
+ url: "http://getfirefox.com",
+ }, {
+ kind: "bookmark",
+ title: "siblingBmk",
+ url: "http://getthunderbird.com",
+ }, {
+ kind: "folder",
+ title: "siblingFolder",
+ }, {
+ kind: "separator",
+ title: "siblingSep",
+ });
+
+ do_print("Reorder inserted bookmarks");
+ {
+ let order = [guids.siblingFolder, guids.siblingSep, guids.childBmk,
+ guids.siblingBmk];
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, order);
+ let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(PlacesUtils.bookmarks.menuGuid);
+ deepEqual(childSyncIds, order, "New bookmarks should be reordered according to array");
+ }
+
+ do_print("Reorder with unspecified children");
+ {
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [
+ guids.siblingSep, guids.siblingBmk,
+ ]);
+ let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.menuGuid);
+ deepEqual(childSyncIds, [guids.siblingSep, guids.siblingBmk,
+ guids.siblingFolder, guids.childBmk],
+ "Unordered children should be moved to end");
+ }
+
+ do_print("Reorder with nonexistent children");
+ {
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.menuGuid, [
+ guids.childBmk, makeGuid(), guids.siblingBmk, guids.siblingSep,
+ makeGuid(), guids.siblingFolder, makeGuid()]);
+ let childSyncIds = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.menuGuid);
+ deepEqual(childSyncIds, [guids.childBmk, guids.siblingBmk, guids.siblingSep,
+ guids.siblingFolder], "Nonexistent children should be ignored");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_changeGuid_invalid() {
+ yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid()),
+ "Should require a new GUID");
+ yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid(), "!@#$"),
+ "Should reject invalid GUIDs");
+ yield rejects(PlacesSyncUtils.bookmarks.changeGuid(makeGuid(), makeGuid()),
+ "Should reject nonexistent item GUIDs");
+ yield rejects(
+ PlacesSyncUtils.bookmarks.changeGuid(PlacesUtils.bookmarks.menuGuid,
+ makeGuid()),
+ "Should reject roots");
+});
+
+add_task(function* test_changeGuid() {
+ let item = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "https://mozilla.org",
+ });
+ let id = yield PlacesUtils.promiseItemId(item.guid);
+
+ let newGuid = makeGuid();
+ let result = yield PlacesSyncUtils.bookmarks.changeGuid(item.guid, newGuid);
+ equal(result, newGuid, "Should return new GUID");
+
+ equal(yield PlacesUtils.promiseItemId(newGuid), id, "Should map ID to new GUID");
+ yield rejects(PlacesUtils.promiseItemId(item.guid), "Should not map ID to old GUID");
+ equal(yield PlacesUtils.promiseItemGuid(id), newGuid, "Should map new GUID to ID");
+});
+
+add_task(function* test_order_roots() {
+ let oldOrder = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.rootGuid);
+ yield PlacesSyncUtils.bookmarks.order(PlacesUtils.bookmarks.rootGuid,
+ shuffle(oldOrder));
+ let newOrder = yield PlacesSyncUtils.bookmarks.fetchChildSyncIds(
+ PlacesUtils.bookmarks.rootGuid);
+ deepEqual(oldOrder, newOrder, "Should ignore attempts to reorder roots");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_tags() {
+ do_print("Insert item without tags");
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://mozilla.org",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ });
+
+ do_print("Add tags");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: ["foo", "bar"],
+ });
+ deepEqual(updatedItem.tags, ["foo", "bar"], "Should return new tags");
+ assertURLHasTags("https://mozilla.org", ["bar", "foo"],
+ "Should set new tags for URL");
+ }
+
+ do_print("Add new tag, remove existing tag");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: ["foo", "baz"],
+ });
+ deepEqual(updatedItem.tags, ["foo", "baz"], "Should return updated tags");
+ assertURLHasTags("https://mozilla.org", ["baz", "foo"],
+ "Should update tags for URL");
+ assertTagForURLs("bar", [], "Should remove existing tag");
+ }
+
+ do_print("Tags with whitespace");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: [" leading", "trailing ", " baz ", " "],
+ });
+ deepEqual(updatedItem.tags, ["leading", "trailing", "baz"],
+ "Should return filtered tags");
+ assertURLHasTags("https://mozilla.org", ["baz", "leading", "trailing"],
+ "Should trim whitespace and filter blank tags");
+ }
+
+ do_print("Remove all tags");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ tags: null,
+ });
+ deepEqual(updatedItem.tags, [], "Should return empty tag array");
+ assertURLHasTags("https://mozilla.org", [],
+ "Should remove all existing tags");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_keyword() {
+ do_print("Insert item without keyword");
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: "menu",
+ url: "https://mozilla.org",
+ syncId: makeGuid(),
+ });
+
+ do_print("Add item keyword");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: "moz",
+ });
+ equal(updatedItem.keyword, "moz", "Should return new keyword");
+ let entryByKeyword = yield PlacesUtils.keywords.fetch("moz");
+ equal(entryByKeyword.url.href, "https://mozilla.org/",
+ "Should set new keyword for URL");
+ let entryByURL = yield PlacesUtils.keywords.fetch({
+ url: "https://mozilla.org",
+ });
+ equal(entryByURL.keyword, "moz", "Looking up URL should return new keyword");
+ }
+
+ do_print("Change item keyword");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: "m",
+ });
+ equal(updatedItem.keyword, "m", "Should return updated keyword");
+ let newEntry = yield PlacesUtils.keywords.fetch("m");
+ equal(newEntry.url.href, "https://mozilla.org/", "Should update keyword for URL");
+ let oldEntry = yield PlacesUtils.keywords.fetch("moz");
+ ok(!oldEntry, "Should remove old keyword");
+ }
+
+ do_print("Remove existing keyword");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: null,
+ });
+ ok(!updatedItem.keyword,
+ "Should not include removed keyword in properties");
+ let entry = yield PlacesUtils.keywords.fetch({
+ url: "https://mozilla.org",
+ });
+ ok(!entry, "Should remove new keyword from URL");
+ }
+
+ do_print("Remove keyword for item without keyword");
+ {
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: item.syncId,
+ keyword: null,
+ });
+ let entry = yield PlacesUtils.keywords.fetch({
+ url: "https://mozilla.org",
+ });
+ ok(!entry,
+ "Removing keyword for URL without existing keyword should succeed");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_annos() {
+ let guids = yield populateTree(PlacesUtils.bookmarks.menuGuid, {
+ kind: "folder",
+ title: "folder",
+ }, {
+ kind: "bookmark",
+ title: "bmk",
+ url: "https://example.com",
+ });
+
+ do_print("Add folder description");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.folder,
+ description: "Folder description",
+ });
+ equal(updatedItem.description, "Folder description",
+ "Should return new description");
+ let id = yield syncIdToId(updatedItem.syncId);
+ equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO),
+ "Folder description", "Should set description anno");
+ }
+
+ do_print("Clear folder description");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.folder,
+ description: null,
+ });
+ ok(!updatedItem.description, "Should not return cleared description");
+ let id = yield syncIdToId(updatedItem.syncId);
+ ok(!PlacesUtils.annotations.itemHasAnnotation(id, DESCRIPTION_ANNO),
+ "Should remove description anno");
+ }
+
+ do_print("Add bookmark sidebar anno");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.bmk,
+ loadInSidebar: true,
+ });
+ ok(updatedItem.loadInSidebar, "Should return sidebar anno");
+ let id = yield syncIdToId(updatedItem.syncId);
+ ok(PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should set sidebar anno for existing bookmark");
+ }
+
+ do_print("Clear bookmark sidebar anno");
+ {
+ let updatedItem = yield PlacesSyncUtils.bookmarks.update({
+ syncId: guids.bmk,
+ loadInSidebar: false,
+ });
+ ok(!updatedItem.loadInSidebar, "Should not return cleared sidebar anno");
+ let id = yield syncIdToId(updatedItem.syncId);
+ ok(!PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should clear sidebar anno for existing bookmark");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_move_root() {
+ do_print("Move root to same parent");
+ {
+ // This should be a no-op.
+ let sameRoot = yield PlacesSyncUtils.bookmarks.update({
+ syncId: "menu",
+ parentSyncId: "places",
+ });
+ equal(sameRoot.syncId, "menu",
+ "Menu root GUID should not change");
+ equal(sameRoot.parentSyncId, "places",
+ "Parent Places root GUID should not change");
+ }
+
+ do_print("Try reparenting root");
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: "menu",
+ parentSyncId: "toolbar",
+ }));
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert() {
+ do_print("Insert bookmark");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ url: "https://example.org",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "Bookmark should have correct type");
+ }
+
+ do_print("Insert query");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ url: "place:terms=term&folder=TOOLBAR&queryType=1",
+ folder: "Saved search",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "Queries should be stored as bookmarks");
+ }
+
+ do_print("Insert folder");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ title: "New folder",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Folder should have correct type");
+ }
+
+ do_print("Insert separator");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "separator",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ });
+ let { type } = yield PlacesUtils.bookmarks.fetch({ guid: item.syncId });
+ equal(type, PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ "Separator should have correct type");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_livemark() {
+ let { site, stopServer } = makeLivemarkServer();
+
+ try {
+ do_print("Insert livemark with feed URL");
+ {
+ let livemark = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "livemark",
+ syncId: makeGuid(),
+ feed: site + "/feed/1",
+ parentSyncId: "menu",
+ });
+ let bmk = yield PlacesUtils.bookmarks.fetch({
+ guid: yield PlacesSyncUtils.bookmarks.syncIdToGuid(livemark.syncId),
+ })
+ equal(bmk.type, PlacesUtils.bookmarks.TYPE_FOLDER,
+ "Livemarks should be stored as folders");
+ }
+
+ let livemarkSyncId;
+ do_print("Insert livemark with site and feed URLs");
+ {
+ let livemark = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "livemark",
+ syncId: makeGuid(),
+ site,
+ feed: site + "/feed/1",
+ parentSyncId: "menu",
+ });
+ livemarkSyncId = livemark.syncId;
+ }
+
+ do_print("Try inserting livemark into livemark");
+ {
+ let livemark = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "livemark",
+ syncId: makeGuid(),
+ site,
+ feed: site + "/feed/1",
+ parentSyncId: livemarkSyncId,
+ });
+ ok(!livemark, "Should not insert livemark as child of livemark");
+ }
+ } finally {
+ yield stopServer();
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_update_livemark() {
+ let { site, stopServer } = makeLivemarkServer();
+ let feedURI = uri(site + "/feed/1");
+
+ try {
+ // We shouldn't reinsert the livemark if the URLs are the same.
+ do_print("Update livemark with same URLs");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ feed: feedURI,
+ });
+ // `nsLivemarkService` returns references to `Livemark` instances, so we
+ // can compare them with `==` to make sure they haven't been replaced.
+ equal(yield PlacesUtils.livemarks.getLivemark({
+ guid: livemark.guid,
+ }), livemark, "Livemark with same feed URL should not be replaced");
+
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site,
+ });
+ equal(yield PlacesUtils.livemarks.getLivemark({
+ guid: livemark.guid,
+ }), livemark, "Livemark with same site URL should not be replaced");
+
+ yield PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ feed: feedURI,
+ site,
+ });
+ equal(yield PlacesUtils.livemarks.getLivemark({
+ guid: livemark.guid,
+ }), livemark, "Livemark with same feed and site URLs should not be replaced");
+ }
+
+ do_print("Change livemark feed URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ // Since we're reinserting, we need to pass all properties required
+ // for a new livemark. `update` won't merge the old and new ones.
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ feed: site + "/feed/2",
+ }), "Reinserting livemark with changed feed URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed: site + "/feed/2",
+ });
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark with changed feed URL");
+ equal(newLivemark.feed.href, site + "/feed/2",
+ "Reinserted livemark should have changed feed URI");
+ }
+
+ do_print("Add livemark site URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ });
+ ok(livemark.feedURI.equals(feedURI), "Livemark feed URI should match");
+ ok(!livemark.siteURI, "Livemark should not have site URI");
+
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site,
+ }), "Reinserting livemark with new site URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed: feedURI,
+ site,
+ });
+ notEqual(newLivemark, livemark,
+ "Livemark with new site URL should replace old livemark");
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark with new site URL");
+ equal(newLivemark.site.href, site + "/",
+ "Reinserted livemark should have new site URI");
+ equal(newLivemark.feed.href, feedURI.spec,
+ "Reinserted livemark with new site URL should have same feed URI");
+ }
+
+ do_print("Remove livemark site URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site: null,
+ }), "Reinserting livemark witout site URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed: feedURI,
+ site: null,
+ });
+ notEqual(newLivemark, livemark,
+ "Livemark without site URL should replace old livemark");
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark without site URL");
+ ok(!newLivemark.site, "Reinserted livemark should not have site URI");
+ }
+
+ do_print("Change livemark site URL");
+ {
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI,
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+
+ yield rejects(PlacesSyncUtils.bookmarks.update({
+ syncId: livemark.guid,
+ site: site + "/new",
+ }), "Reinserting livemark with changed site URL requires full record");
+
+ let newLivemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: livemark.guid,
+ feed:feedURI,
+ site: site + "/new",
+ });
+ notEqual(newLivemark, livemark,
+ "Livemark with changed site URL should replace old livemark");
+ equal(newLivemark.syncId, livemark.guid,
+ "GUIDs should match for reinserted livemark with changed site URL");
+ equal(newLivemark.site.href, site + "/new",
+ "Reinserted livemark should have changed site URI");
+ }
+
+ // Livemarks are stored as folders, but have different kinds. We should
+ // remove the folder and insert a livemark with the same GUID instead of
+ // trying to update the folder in-place.
+ do_print("Replace folder with livemark");
+ {
+ let folder = yield PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "Plain folder",
+ });
+ let livemark = yield PlacesSyncUtils.bookmarks.update({
+ kind: "livemark",
+ parentSyncId: "menu",
+ syncId: folder.guid,
+ feed: feedURI,
+ });
+ equal(livemark.guid, folder.syncId,
+ "Livemark should have same GUID as replaced folder");
+ }
+ } finally {
+ yield stopServer();
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_tags() {
+ yield Promise.all([{
+ kind: "bookmark",
+ url: "https://example.com",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ tags: ["foo", "bar"],
+ }, {
+ kind: "bookmark",
+ url: "https://example.org",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ tags: ["foo", "baz"],
+ }, {
+ kind: "query",
+ url: "place:queryType=1&sort=12&maxResults=10",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ folder: "bar",
+ tags: ["baz", "qux"],
+ title: "bar",
+ }].map(info => PlacesSyncUtils.bookmarks.insert(info)));
+
+ assertTagForURLs("foo", ["https://example.com/", "https://example.org/"],
+ "2 URLs with new tag");
+ assertTagForURLs("bar", ["https://example.com/"], "1 URL with existing tag");
+ assertTagForURLs("baz", ["https://example.org/",
+ "place:queryType=1&sort=12&maxResults=10"],
+ "Should support tagging URLs and tag queries");
+ assertTagForURLs("qux", ["place:queryType=1&sort=12&maxResults=10"],
+ "Should support tagging tag queries");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_tags_whitespace() {
+ do_print("Untrimmed and blank tags");
+ let taggedBlanks = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.org",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ tags: [" untrimmed ", " ", "taggy"],
+ });
+ deepEqual(taggedBlanks.tags, ["untrimmed", "taggy"],
+ "Should not return empty tags");
+ assertURLHasTags("https://example.org/", ["taggy", "untrimmed"],
+ "Should set trimmed tags and ignore dupes");
+
+ do_print("Dupe tags");
+ let taggedDupes = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.net",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ tags: [" taggy", "taggy ", " taggy ", "taggy"],
+ });
+ deepEqual(taggedDupes.tags, ["taggy", "taggy", "taggy", "taggy"],
+ "Should return trimmed and dupe tags");
+ assertURLHasTags("https://example.net/", ["taggy"],
+ "Should ignore dupes when setting tags");
+
+ assertTagForURLs("taggy", ["https://example.net/", "https://example.org/"],
+ "Should exclude falsy tags");
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_keyword() {
+ do_print("Insert item with new keyword");
+ {
+ yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: "menu",
+ url: "https://example.com",
+ keyword: "moz",
+ syncId: makeGuid(),
+ });
+ let entry = yield PlacesUtils.keywords.fetch("moz");
+ equal(entry.url.href, "https://example.com/",
+ "Should add keyword for item");
+ }
+
+ do_print("Insert item with existing keyword");
+ {
+ yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: "menu",
+ url: "https://mozilla.org",
+ keyword: "moz",
+ syncId: makeGuid(),
+ });
+ let entry = yield PlacesUtils.keywords.fetch("moz");
+ equal(entry.url.href, "https://mozilla.org/",
+ "Should reassign keyword to new item");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_annos() {
+ do_print("Bookmark with description");
+ let descBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.com",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ description: "Bookmark description",
+ });
+ {
+ equal(descBmk.description, "Bookmark description",
+ "Should return new bookmark description");
+ let id = yield syncIdToId(descBmk.syncId);
+ equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO),
+ "Bookmark description", "Should set new bookmark description");
+ }
+
+ do_print("Folder with description");
+ let descFolder = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ description: "Folder description",
+ });
+ {
+ equal(descFolder.description, "Folder description",
+ "Should return new folder description");
+ let id = yield syncIdToId(descFolder.syncId);
+ equal(PlacesUtils.annotations.getItemAnnotation(id, DESCRIPTION_ANNO),
+ "Folder description", "Should set new folder description");
+ }
+
+ do_print("Bookmark with sidebar anno");
+ let sidebarBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.com",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ loadInSidebar: true,
+ });
+ {
+ ok(sidebarBmk.loadInSidebar, "Should return sidebar anno for new bookmark");
+ let id = yield syncIdToId(sidebarBmk.syncId);
+ ok(PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should set sidebar anno for new bookmark");
+ }
+
+ do_print("Bookmark without sidebar anno");
+ let noSidebarBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ url: "https://example.org",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ loadInSidebar: false,
+ });
+ {
+ ok(!noSidebarBmk.loadInSidebar,
+ "Should not return sidebar anno for new bookmark");
+ let id = yield syncIdToId(noSidebarBmk.syncId);
+ ok(!PlacesUtils.annotations.itemHasAnnotation(id, LOAD_IN_SIDEBAR_ANNO),
+ "Should not set sidebar anno for new bookmark");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_tag_query() {
+ let tagFolder = -1;
+
+ do_print("Insert tag query for new tag");
+ {
+ deepEqual(PlacesUtils.tagging.allTags, [], "New tag should not exist yet");
+ let query = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ url: "place:type=7&folder=90",
+ folder: "taggy",
+ title: "Tagged stuff",
+ });
+ notEqual(query.url.href, "place:type=7&folder=90",
+ "Tag query URL for new tag should differ");
+
+ [, tagFolder] = /\bfolder=(\d+)\b/.exec(query.url.pathname);
+ ok(tagFolder > 0, "New tag query URL should contain valid folder");
+ deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "New tag should exist");
+ }
+
+ do_print("Insert tag query for existing tag");
+ {
+ let url = "place:type=7&folder=90&maxResults=15";
+ let query = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ url,
+ folder: "taggy",
+ title: "Sorted and tagged",
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ });
+ notEqual(query.url.href, url, "Tag query URL for existing tag should differ");
+ let params = new URLSearchParams(query.url.pathname);
+ equal(params.get("type"), "7", "Should preserve query type");
+ equal(params.get("maxResults"), "15", "Should preserve additional params");
+ equal(params.get("folder"), tagFolder, "Should update tag folder");
+ deepEqual(PlacesUtils.tagging.allTags, ["taggy"], "Should not duplicate existing tags");
+ }
+
+ do_print("Use the public tagging API to ensure we added the tag correctly");
+ {
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "https://mozilla.org",
+ title: "Mozilla",
+ });
+ PlacesUtils.tagging.tagURI(uri("https://mozilla.org"), ["taggy"]);
+ assertURLHasTags("https://mozilla.org/", ["taggy"],
+ "Should set tags using the tagging API");
+ }
+
+ do_print("Removing the tag should clean up the tag folder");
+ {
+ PlacesUtils.tagging.untagURI(uri("https://mozilla.org"), null);
+ deepEqual(PlacesUtils.tagging.allTags, [],
+ "Should remove tag folder once last item is untagged");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_insert_orphans() {
+ let grandParentGuid = makeGuid();
+ let parentGuid = makeGuid();
+ let childGuid = makeGuid();
+ let childId;
+
+ do_print("Insert an orphaned child");
+ {
+ let child = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "bookmark",
+ parentSyncId: parentGuid,
+ syncId: childGuid,
+ url: "https://mozilla.org",
+ });
+ equal(child.syncId, childGuid,
+ "Should insert orphan with requested GUID");
+ equal(child.parentSyncId, "unfiled",
+ "Should reparent orphan to unfiled");
+
+ childId = yield PlacesUtils.promiseItemId(childGuid);
+ equal(PlacesUtils.annotations.getItemAnnotation(childId, SYNC_PARENT_ANNO),
+ parentGuid, "Should set anno to missing parent GUID");
+ }
+
+ do_print("Insert the grandparent");
+ {
+ yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ parentSyncId: "menu",
+ syncId: grandParentGuid,
+ });
+ equal(PlacesUtils.annotations.getItemAnnotation(childId, SYNC_PARENT_ANNO),
+ parentGuid, "Child should still have orphan anno");
+ }
+
+ // Note that only `PlacesSyncUtils` reparents orphans, though Sync adds an
+ // observer that removes the orphan anno if the orphan is manually moved.
+ do_print("Insert the missing parent");
+ {
+ let parent = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "folder",
+ parentSyncId: grandParentGuid,
+ syncId: parentGuid,
+ });
+ equal(parent.syncId, parentGuid, "Should insert parent with requested GUID");
+ equal(parent.parentSyncId, grandParentGuid,
+ "Parent should be child of grandparent");
+ ok(!PlacesUtils.annotations.itemHasAnnotation(childId, SYNC_PARENT_ANNO),
+ "Orphan anno should be removed after reparenting");
+
+ let child = yield PlacesUtils.bookmarks.fetch({ guid: childGuid });
+ equal(child.parentGuid, parentGuid,
+ "Should reparent child after inserting missing parent");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_fetch() {
+ let folder = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ kind: "folder",
+ description: "Folder description",
+ });
+ let bmk = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: "menu",
+ kind: "bookmark",
+ url: "https://example.com",
+ description: "Bookmark description",
+ loadInSidebar: true,
+ tags: ["taggy"],
+ });
+ let folderBmk = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: folder.syncId,
+ kind: "bookmark",
+ url: "https://example.org",
+ keyword: "kw",
+ });
+ let folderSep = yield PlacesSyncUtils.bookmarks.insert({
+ syncId: makeGuid(),
+ parentSyncId: folder.syncId,
+ kind: "separator",
+ });
+ let tagQuery = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ url: "place:type=7&folder=90",
+ folder: "taggy",
+ title: "Tagged stuff",
+ });
+ let [, tagFolderId] = /\bfolder=(\d+)\b/.exec(tagQuery.url.pathname);
+ let smartBmk = yield PlacesSyncUtils.bookmarks.insert({
+ kind: "query",
+ syncId: makeGuid(),
+ parentSyncId: "toolbar",
+ url: "place:folder=TOOLBAR",
+ query: "BookmarksToolbar",
+ title: "Bookmarks toolbar query",
+ });
+
+ do_print("Fetch empty folder with description");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(folder.syncId);
+ deepEqual(item, {
+ syncId: folder.syncId,
+ kind: "folder",
+ parentSyncId: "menu",
+ description: "Folder description",
+ childSyncIds: [folderBmk.syncId, folderSep.syncId],
+ parentTitle: "Bookmarks Menu",
+ title: "",
+ }, "Should include description, children, title, and parent title in folder");
+ }
+
+ do_print("Fetch bookmark with description, sidebar anno, and tags");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(bmk.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "tags", "description", "loadInSidebar", "parentTitle", "title"].sort(),
+ "Should include bookmark-specific properties");
+ equal(item.syncId, bmk.syncId, "Sync ID should match");
+ equal(item.url.href, "https://example.com/", "Should return URL");
+ equal(item.parentSyncId, "menu", "Should return parent sync ID");
+ deepEqual(item.tags, ["taggy"], "Should return tags");
+ equal(item.description, "Bookmark description", "Should return bookmark description");
+ strictEqual(item.loadInSidebar, true, "Should return sidebar anno");
+ equal(item.parentTitle, "Bookmarks Menu", "Should return parent title");
+ strictEqual(item.title, "", "Should return empty title");
+ }
+
+ do_print("Fetch bookmark with keyword; without parent title or annos");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(folderBmk.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "keyword", "tags", "loadInSidebar", "parentTitle", "title"].sort(),
+ "Should omit blank bookmark-specific properties");
+ strictEqual(item.loadInSidebar, false, "Should not load bookmark in sidebar");
+ deepEqual(item.tags, [], "Tags should be empty");
+ equal(item.keyword, "kw", "Should return keyword");
+ strictEqual(item.parentTitle, "", "Should include parent title even if empty");
+ strictEqual(item.title, "", "Should include bookmark title even if empty");
+ }
+
+ do_print("Fetch separator");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(folderSep.syncId);
+ strictEqual(item.index, 1, "Should return separator position");
+ }
+
+ do_print("Fetch tag query");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(tagQuery.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "title", "folder", "parentTitle"].sort(),
+ "Should include query-specific properties");
+ equal(item.url.href, `place:type=7&folder=${tagFolderId}`, "Should not rewrite outgoing tag queries");
+ equal(item.folder, "taggy", "Should return tag name for tag queries");
+ }
+
+ do_print("Fetch smart bookmark");
+ {
+ let item = yield PlacesSyncUtils.bookmarks.fetch(smartBmk.syncId);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "url", "title", "query", "parentTitle"].sort(),
+ "Should include smart bookmark-specific properties");
+ equal(item.query, "BookmarksToolbar", "Should return query name for smart bookmarks");
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(function* test_fetch_livemark() {
+ let { site, stopServer } = makeLivemarkServer();
+
+ try {
+ do_print("Create livemark");
+ let livemark = yield PlacesUtils.livemarks.addLivemark({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ feedURI: uri(site + "/feed/1"),
+ siteURI: uri(site),
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ });
+ PlacesUtils.annotations.setItemAnnotation(livemark.id, DESCRIPTION_ANNO,
+ "Livemark description", 0, PlacesUtils.annotations.EXPIRE_NEVER);
+
+ do_print("Fetch livemark");
+ let item = yield PlacesSyncUtils.bookmarks.fetch(livemark.guid);
+ deepEqual(Object.keys(item).sort(), ["syncId", "kind", "parentSyncId",
+ "description", "feed", "site", "parentTitle", "title"].sort(),
+ "Should include livemark-specific properties");
+ equal(item.description, "Livemark description", "Should return description");
+ equal(item.feed.href, site + "/feed/1", "Should return feed URL");
+ equal(item.site.href, site + "/", "Should return site URL");
+ strictEqual(item.title, "", "Should include livemark title even if empty");
+ } finally {
+ yield stopServer();
+ }
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
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 000000000..92930e329
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_tag_autocomplete_search.js
@@ -0,0 +1,137 @@
+/* -*- 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 current_test = 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: function(aIndex) {
+ return this.searches[aIndex];
+ },
+
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+
+ popupOpen: false,
+
+ popup: {
+ setSelectedIndex: function(aIndex) {},
+ invalidate: function() {},
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompletePopup))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ },
+
+ // nsISupports implementation
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsISupports) ||
+ iid.equals(Ci.nsIAutoCompleteInput))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// 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");
+}
+
+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;
+
+ var numSearchesStarted = 0;
+ input.onSearchBegin = function input_onSearchBegin() {
+ numSearchesStarted++;
+ do_check_eq(numSearchesStarted, 1);
+ };
+
+ input.onSearchComplete = function input_onSearchComplete() {
+ do_check_eq(numSearchesStarted, 1);
+ if (results.length)
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_MATCH);
+ else
+ do_check_eq(controller.searchStatus,
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH);
+
+ do_check_eq(controller.matchCount, results.length);
+ for (var i=0; i<controller.matchCount; i++) {
+ do_check_eq(controller.getValueAt(i), results[i]);
+ }
+
+ if (current_test < (tests.length - 1)) {
+ current_test++;
+ tests[current_test]();
+ }
+ else {
+ // finish once all tests have run
+ do_test_finished();
+ }
+ };
+
+ controller.startSearch(searchTerm);
+}
+
+var uri1 = uri("http://site.tld/1");
+
+var tests = [
+ function test1() { ensure_tag_results(["bar", "Baz", "boo"], "b"); },
+ function test2() { ensure_tag_results(["bar", "Baz"], "ba"); },
+ function test3() { ensure_tag_results(["bar", "Baz"], "Ba"); },
+ function test4() { ensure_tag_results(["bar"], "bar"); },
+ function test5() { ensure_tag_results(["Baz"], "Baz"); },
+ function test6() { ensure_tag_results([], "barb"); },
+ function test7() { ensure_tag_results([], "foo"); },
+ function test8() { ensure_tag_results(["first tag, bar", "first tag, Baz"], "first tag, ba"); },
+ function test9() { ensure_tag_results(["first tag; bar", "first tag; Baz"], "first tag; ba"); }
+];
+
+/**
+ * Test tag autocomplete
+ */
+function run_test() {
+ // Search is asynchronous, so don't let the test finish immediately
+ do_test_pending();
+
+ tagssvc.tagURI(uri1, ["bar", "Baz", "boo", "*nix"]);
+
+ tests[0]();
+}
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 000000000..ccb287050
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_tagging.js
@@ -0,0 +1,189 @@
+/* -*- 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.setFolders([PlacesUtils.tagsFolderId], 1);
+ var result = PlacesUtils.history.executeQuery(query, options);
+ var tagRoot = result.root;
+ tagRoot.containerOpen = true;
+
+ do_check_eq(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"]);
+ do_check_eq(tagRoot.childCount, 1);
+
+ var tag1node = tagRoot.getChild(0)
+ .QueryInterface(Ci.nsINavHistoryContainerResultNode);
+ var tag1itemId = tag1node.itemId;
+
+ do_check_eq(tag1node.title, "tag 1");
+ tag1node.containerOpen = true;
+ do_check_eq(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"]);
+ do_check_eq(tag1node.childCount, 2);
+ tagssvc.tagURI(uri1, [tag1itemId]);
+ do_check_eq(tag1node.childCount, 2);
+ do_check_eq(tagRoot.childCount, 1);
+
+ // also tests bug 407575
+ tagssvc.tagURI(uri1, [tag1itemId, "tag 1", "tag 2", "Tag 1", "Tag 2"]);
+ do_check_eq(tagRoot.childCount, 2);
+ do_check_eq(tag1node.childCount, 2);
+
+ // test getTagsForURI
+ var uri1tags = tagssvc.getTagsForURI(uri1);
+ do_check_eq(uri1tags.length, 2);
+ do_check_eq(uri1tags[0], "Tag 1");
+ do_check_eq(uri1tags[1], "Tag 2");
+ var uri2tags = tagssvc.getTagsForURI(uri2);
+ do_check_eq(uri2tags.length, 1);
+ do_check_eq(uri2tags[0], "Tag 1");
+
+ // test getURIsForTag
+ var tag1uris = tagssvc.getURIsForTag("tag 1");
+ do_check_eq(tag1uris.length, 2);
+ do_check_true(tag1uris[0].equals(uri1));
+ do_check_true(tag1uris[1].equals(uri2));
+
+ // test allTags attribute
+ var allTags = tagssvc.allTags;
+ do_check_eq(allTags.length, 2);
+ do_check_eq(allTags[0], "Tag 1");
+ do_check_eq(allTags[1], "Tag 2");
+
+ // test untagging
+ tagssvc.untagURI(uri1, ["tag 1"]);
+ do_check_eq(tag1node.childCount, 1);
+
+ // removing the last uri from a tag should remove the tag-container
+ tagssvc.untagURI(uri2, ["tag 1"]);
+ do_check_eq(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);
+ do_check_true(tags.includes(tagTitle));
+ do_check_true(tags.includes("tag 3"));
+ do_check_true(tags.includes("456"));
+
+ // test mixed id/name tagging
+ tagssvc.untagURI(uri3, [tagId, "tag 3", "456"]);
+ tags = tagssvc.getTagsForURI(uri3);
+ do_check_eq(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);
+ do_check_eq(uri4Tags.length, 3);
+ do_check_true(uri4Tags.includes(tagTitle));
+ do_check_true(uri4Tags.includes("tag 3"));
+ do_check_true(uri4Tags.includes("456"));
+
+ // Test sparse arrays.
+ let curChildCount = tagRoot.childCount;
+
+ try {
+ tagssvc.tagURI(uri1, [, "tagSparse"]);
+ do_check_eq(tagRoot.childCount, curChildCount + 1);
+ } catch (ex) {
+ do_throw("Passing a sparse array should not throw");
+ }
+ try {
+ tagssvc.untagURI(uri1, [, "tagSparse"]);
+ do_check_eq(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) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.untagURI(uri1, ["", "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.tagURI(uri1, [0, "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+ try {
+ tagssvc.tagURI(uri1, [0, "test"]);
+ do_throw("Passing a bad tags array should throw");
+ } catch (ex) {
+ do_check_eq(ex.name, "NS_ERROR_ILLEGAL_VALUE");
+ }
+
+ // Tag name length should be limited to nsITaggingService.MAX_TAG_LENGTH (bug407821)
+ try {
+
+ // generate a long tag name. i.e. looooo...oong_tag
+ var n = Ci.nsITaggingService.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) {
+ do_check_eq(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);
+ do_check_eq(exampleTags.length, 1);
+ do_check_eq(exampleTags[0], "test");
+
+ PlacesUtils.tagging.untagURI(exampleURI, [ "test" ]);
+ exampleTags = PlacesUtils.tagging.getTagsForURI(exampleURI);
+ do_check_eq(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 000000000..99f36d78c
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_telemetry.js
@@ -0,0 +1,166 @@
+/* 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.
+
+Components.utils.import("resource://gre/modules/PlacesDBUtils.jsm");
+
+var histograms = {
+ PLACES_PAGES_COUNT: val => do_check_eq(val, 1),
+ PLACES_BOOKMARKS_COUNT: val => do_check_eq(val, 1),
+ PLACES_TAGS_COUNT: val => do_check_eq(val, 1),
+ PLACES_KEYWORDS_COUNT: val => do_check_eq(val, 1),
+ PLACES_SORTED_BOOKMARKS_PERC: val => do_check_eq(val, 100),
+ PLACES_TAGGED_BOOKMARKS_PERC: val => do_check_eq(val, 100),
+ PLACES_DATABASE_FILESIZE_MB: val => do_check_true(val > 0),
+ PLACES_DATABASE_PAGESIZE_B: val => do_check_eq(val, 32768),
+ PLACES_DATABASE_SIZE_PER_PAGE_B: val => do_check_true(val > 0),
+ PLACES_EXPIRATION_STEPS_TO_CLEAN2: val => do_check_true(val > 1),
+ // PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS: val => do_check_true(val > 1),
+ PLACES_IDLE_FRECENCY_DECAY_TIME_MS: val => do_check_true(val >= 0),
+ PLACES_IDLE_MAINTENANCE_TIME_MS: val => do_check_true(val > 0),
+ // One from the `setItemAnnotation` call; the other from the mobile root.
+ // This can be removed along with the anno in bug 1306445.
+ PLACES_ANNOS_BOOKMARKS_COUNT: val => do_check_eq(val, 2),
+ PLACES_ANNOS_PAGES_COUNT: val => do_check_eq(val, 1),
+ PLACES_MAINTENANCE_DAYSFROMLAST: val => do_check_true(val >= 0),
+}
+
+/**
+ * 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(function* test_execute()
+{
+ // Put some trash in the database.
+ let uri = NetUtil.newURI("http://moz.org/");
+
+ let folderId = PlacesUtils.bookmarks.createFolder(PlacesUtils.unfiledBookmarksFolderId,
+ "moz test",
+ PlacesUtils.bookmarks.DEFAULT_INDEX);
+ let itemId = PlacesUtils.bookmarks.insertBookmark(folderId,
+ uri,
+ PlacesUtils.bookmarks.DEFAULT_INDEX,
+ "moz test");
+ PlacesUtils.tagging.tagURI(uri, ["tag"]);
+ yield PlacesUtils.keywords.insert({ url: uri.spec, keyword: "keyword"});
+
+ // Set a large annotation.
+ let content = "";
+ while (content.length < 1024) {
+ content += "0";
+ }
+ PlacesUtils.annotations.setItemAnnotation(itemId, "test-anno", content, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+ PlacesUtils.annotations.setPageAnnotation(uri, "test-anno", content, 0,
+ PlacesUtils.annotations.EXPIRE_NEVER);
+
+ // Request to gather telemetry data.
+ Cc["@mozilla.org/places/categoriesStarter;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "gather-telemetry", null);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+
+ // Test expiration probes.
+ let timeInMicroseconds = getExpirablePRTime(8);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ for (let i = 0; i < 3; i++) {
+ yield PlacesTestUtils.addVisits({
+ uri: NetUtil.newURI("http://" + i + ".moz.org/"),
+ visitDate: newTimeInMicroseconds()
+ });
+ }
+ Services.prefs.setIntPref("places.history.expiration.max_pages", 0);
+ yield promiseForceExpirationStep(2);
+ yield promiseForceExpirationStep(2);
+
+ // Test autocomplete probes.
+ /*
+ // This is useful for manual testing by changing the minimum time for
+ // autocomplete telemetry to 0, but there is no way to artificially delay
+ // autocomplete by more than 50ms in a realiable way.
+ Services.prefs.setIntPref("browser.urlbar.search.sources", 3);
+ Services.prefs.setIntPref("browser.urlbar.default.behavior", 0);
+ function AutoCompleteInput(aSearches) {
+ this.searches = aSearches;
+ }
+ AutoCompleteInput.prototype = {
+ timeout: 10,
+ textValue: "",
+ searchParam: "",
+ popupOpen: false,
+ minResultsForPopup: 0,
+ invalidate: function() {},
+ disableAutoComplete: false,
+ completeDefaultIndex: false,
+ get popup() { return this; },
+ onSearchBegin: function() {},
+ onSearchComplete: function() {},
+ setSelectedIndex: function() {},
+ get searchCount() { return this.searches.length; },
+ getSearchAt: function(aIndex) { return this.searches[aIndex]; },
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIAutoCompleteInput,
+ Ci.nsIAutoCompletePopup,
+ ])
+ };
+ let controller = Cc["@mozilla.org/autocomplete/controller;1"].
+ getService(Ci.nsIAutoCompleteController);
+ controller.input = new AutoCompleteInput(["unifiedcomplete"]);
+ controller.startSearch("moz");
+ */
+
+ // Test idle probes.
+ PlacesUtils.history.QueryInterface(Ci.nsIObserver)
+ .observe(null, "idle-daily", null);
+ PlacesDBUtils.maintenanceOnIdle();
+
+ yield promiseTopicObserved("places-maintenance-finished");
+
+ for (let histogramId in histograms) {
+ do_print("checking histogram " + histogramId);
+ let validate = histograms[histogramId];
+ let snapshot = Services.telemetry.getHistogramById(histogramId).snapshot();
+ validate(snapshot.sum);
+ do_check_true(snapshot.counts.reduce((a, b) => a + b) > 0);
+ }
+});
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 000000000..662ea0841
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_update_frecency_after_delete.js
@@ -0,0 +1,151 @@
+/* -*- 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(function* removed_bookmark() {
+ do_print("After removing bookmark, frecency of bookmark's URI should be " +
+ "zero if URI is unvisited and no longer bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.remove(bm);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Unvisited URI no longer bookmarked => frecency should = 0");
+ do_check_eq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* removed_but_visited_bookmark() {
+ do_print("After removing bookmark, frecency of bookmark's URI should " +
+ "not be zero if URI is visited.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let bm = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ yield PlacesUtils.bookmarks.remove(bm);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("*Visited* URI no longer bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* remove_bookmark_still_bookmarked() {
+ do_print("After removing bookmark, frecency of bookmark's URI should " +
+ "not be zero if URI is still bookmarked.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ let bm1 = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 1 title",
+ url: TEST_URI
+ });
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark 2 title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.remove(bm1);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("URI still bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* cleared_parent_of_visited_bookmark() {
+ do_print("After removing all children from bookmark's parent, frecency " +
+ "of bookmark's URI should not be zero if URI is visited.");
+ const TEST_URI = NetUtil.newURI("http://example.com/1");
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmark title",
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesTestUtils.addVisits(TEST_URI);
+ PlacesUtils.bookmarks.removeFolderChildren(PlacesUtils.unfiledBookmarksFolderId);
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("*Visited* URI no longer bookmarked => frecency should != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
+
+add_task(function* cleared_parent_of_bookmark_still_bookmarked() {
+ do_print("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 = NetUtil.newURI("http://example.com/1");
+ yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "bookmark 1 title",
+ url: TEST_URI
+ });
+
+ let folder = yield PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "bookmark 2 folder"
+ });
+ yield PlacesUtils.bookmarks.insert({
+ title: "bookmark 2 title",
+ parentGuid: folder.guid,
+ url: TEST_URI
+ });
+
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ do_print("Bookmarked => frecency of URI should be != 0");
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.remove(folder);
+ yield PlacesTestUtils.promiseAsyncUpdates();
+ // URI still bookmarked => frecency should != 0.
+ do_check_neq(frecencyForUrl(TEST_URI), 0);
+
+ yield PlacesUtils.bookmarks.eraseEverything();
+ yield PlacesTestUtils.clearHistory();
+});
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 000000000..a30589c44
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_backups_create.js
@@ -0,0 +1,90 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /**
+ * Check for correct functionality of bookmarks backups
+ */
+
+const NUMBER_OF_BACKUPS = 10;
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ // Generate random dates.
+ let dateObj = new Date();
+ let dates = [];
+ while (dates.length < NUMBER_OF_BACKUPS) {
+ // 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();
+
+ // Get and cleanup the backups folder.
+ let backupFolderPath = yield PlacesBackups.getBackupFolder();
+ let bookmarksBackupDir = new FileUtils.File(backupFolderPath);
+
+ // 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));
+ do_print("Creating fake backup " + backupFile.leafName);
+ if (!backupFile.exists())
+ do_throw("Unable to create fake backup " + backupFile.leafName);
+ }
+
+ yield PlacesBackups.create(NUMBER_OF_BACKUPS);
+ // Add today's backup.
+ dates.push(dateObj.getTime());
+
+ // 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.getNext().QueryInterface(Ci.nsIFile);
+ 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);
+ }
+
+ // 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.getNext().QueryInterface(Ci.nsIFile);
+ entry.remove(false);
+ }
+ do_check_false(bookmarksBackupDir.directoryEntries.hasMoreElements());
+});
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 000000000..ecebce94a
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_getURLsForContainerNode.js
@@ -0,0 +1,180 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /**
+ * 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;
+var bs = PU.bookmarks;
+
+var tests = [
+
+function() {
+ dump("\n\n*** TEST: folder\n");
+ // This is the folder we will check for children.
+ var folderId = bs.createFolder(bs.toolbarFolder, "folder", bs.DEFAULT_INDEX);
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(folderId, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(folderId, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(folderId, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: folder in an excludeItems root\n");
+ // This is the folder we will check for children.
+ var folderId = bs.createFolder(bs.toolbarFolder, "folder", bs.DEFAULT_INDEX);
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(folderId, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(folderId, uri("place:sort=1"), bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+ options.excludeItems = true;
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(folderId, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: query\n");
+ // This is the query we will check for children.
+ bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+
+ dump("Check query without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check query with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: excludeItems Query\n");
+ // This is the query we will check for children.
+ bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=8"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+ options.excludeItems = true;
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "bookmark");
+ check_uri_nodes(query, options, 1);
+},
+
+function() {
+ dump("\n\n*** TEST: !expandQueries Query\n");
+ // This is the query we will check for children.
+ bs.insertBookmark(bs.toolbarFolder, uri("place:folder=BOOKMARKS_MENU&sort=8"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ // Create a folder and a query node inside it, these should not be considered
+ // uri nodes.
+ bs.createFolder(bs.bookmarksMenuFolder, "inside folder", bs.DEFAULT_INDEX);
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("place:sort=1"),
+ bs.DEFAULT_INDEX, "inside query");
+
+ var query = hs.getNewQuery();
+ query.setFolders([bs.toolbarFolder], 1);
+ var options = hs.getNewQueryOptions();
+ options.expandQueries = false;
+
+ dump("Check folder without uri nodes\n");
+ check_uri_nodes(query, options, 0);
+
+ dump("Check folder with uri nodes\n");
+ // Add an uri node, this should be considered.
+ bs.insertBookmark(bs.bookmarksMenuFolder, uri("http://www.mozilla.org/"),
+ bs.DEFAULT_INDEX, "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);
+ do_check_eq(PU.hasChildURIs(node), aExpectedURINodes > 0);
+ do_check_eq(PU.getURLsForContainerNode(node).length, aExpectedURINodes);
+ root.containerOpen = false;
+}
+
+add_task(function* () {
+ for (let test of tests) {
+ yield PlacesUtils.bookmarks.eraseEverything();
+ test();
+ }
+
+ // Cleanup.
+ yield PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js b/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js
new file mode 100644
index 000000000..62947620d
--- /dev/null
+++ b/toolkit/components/places/tests/unit/test_utils_setAnnotationsFor.js
@@ -0,0 +1,79 @@
+/* -*- 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.setAnnotationsForItem/URI
+ */
+
+var hs = PlacesUtils.history;
+var bs = PlacesUtils.bookmarks;
+var as = PlacesUtils.annotations;
+
+const TEST_URL = "http://test.mozilla.org/";
+
+function run_test() {
+ var testURI = uri(TEST_URL);
+ // add a bookmark
+ var itemId = bs.insertBookmark(bs.unfiledBookmarksFolder, testURI,
+ bs.DEFAULT_INDEX, "test");
+
+ // create annotations array
+ var testAnnos = [{ name: "testAnno/test0",
+ flags: 0,
+ value: "test0",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER },
+ { name: "testAnno/test1",
+ flags: 0,
+ value: "test1",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER },
+ { name: "testAnno/test2",
+ flags: 0,
+ value: "test2",
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER },
+ { name: "testAnno/test3",
+ flags: 0,
+ value: 0,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
+
+ // Add item annotations
+ PlacesUtils.setAnnotationsForItem(itemId, testAnnos);
+ // Check for correct addition
+ testAnnos.forEach(function(anno) {
+ do_check_true(as.itemHasAnnotation(itemId, anno.name));
+ do_check_eq(as.getItemAnnotation(itemId, anno.name), anno.value);
+ });
+
+ // Add page annotations
+ PlacesUtils.setAnnotationsForURI(testURI, testAnnos);
+ // Check for correct addition
+ testAnnos.forEach(function(anno) {
+ do_check_true(as.pageHasAnnotation(testURI, anno.name));
+ do_check_eq(as.getPageAnnotation(testURI, anno.name), anno.value);
+ });
+
+ // To unset annotations we unset their values or set them to
+ // null/undefined
+ testAnnos[0].value = null;
+ testAnnos[1].value = undefined;
+ delete testAnnos[2].value;
+ delete testAnnos[3].value;
+
+ // Unset all item annotations
+ PlacesUtils.setAnnotationsForItem(itemId, testAnnos);
+ // Check for correct removal
+ testAnnos.forEach(function(anno) {
+ do_check_false(as.itemHasAnnotation(itemId, anno.name));
+ // sanity: page annotations should not be removed here
+ do_check_true(as.pageHasAnnotation(testURI, anno.name));
+ });
+
+ // Unset all page annotations
+ PlacesUtils.setAnnotationsForURI(testURI, testAnnos);
+ // Check for correct removal
+ testAnnos.forEach(function(anno) {
+ do_check_false(as.pageHasAnnotation(testURI, anno.name));
+ });
+}
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 000000000..3cab39ed9
--- /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(function* test_execute() {
+ const TEST_URI = uri("http://mozilla.com");
+
+ do_check_eq(0, yield PlacesTestUtils.visitsInDB(TEST_URI));
+ yield PlacesTestUtils.addVisits({uri: TEST_URI});
+ do_check_eq(1, yield PlacesTestUtils.visitsInDB(TEST_URI));
+ yield PlacesTestUtils.addVisits({uri: TEST_URI});
+ do_check_eq(2, yield PlacesTestUtils.visitsInDB(TEST_URI));
+});
diff --git a/toolkit/components/places/tests/unit/xpcshell.ini b/toolkit/components/places/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..60bba4758
--- /dev/null
+++ b/toolkit/components/places/tests/unit/xpcshell.ini
@@ -0,0 +1,163 @@
+[DEFAULT]
+head = head_bookmarks.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ bookmarks.corrupt.html
+ bookmarks.json
+ bookmarks.preplaces.html
+ bookmarks_html_singleframe.html
+ bug476292.sqlite
+ corruptDB.sqlite
+ default.sqlite
+ livemark.xml
+ 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
+ nsDummyObserver.js
+ nsDummyObserver.manifest
+ places.sparse.sqlite
+
+[test_000_frecency.js]
+[test_317472.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_331487.js]
+[test_384370.js]
+[test_385397.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_399264_query_to_string.js]
+[test_399264_string_to_query.js]
+[test_399266.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+# Bug 821781: test fails intermittently on Linux
+skip-if = os == "linux"
+[test_402799.js]
+[test_405497.js]
+[test_408221.js]
+[test_412132.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_413784.js]
+[test_415460.js]
+[test_415757.js]
+[test_418643_removeFolderChildren.js]
+[test_419731.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_452777.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_1085291.js]
+[test_1105208.js]
+[test_1105866.js]
+[test_adaptive.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_adaptive_bug527311.js]
+[test_analyze.js]
+[test_annotations.js]
+[test_asyncExecuteLegacyQueries.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_async_history_api.js]
+[test_async_in_batchmode.js]
+[test_async_transactions.js]
+skip-if = (os == "win" && os_version == "5.1") # Bug 1158887
+[test_autocomplete_stopSearch_no_throw.js]
+[test_bookmark_catobs.js]
+[test_bookmarks_json.js]
+[test_bookmarks_html.js]
+[test_bookmarks_html_corrupt.js]
+[test_bookmarks_html_import_tags.js]
+[test_bookmarks_html_singleframe.js]
+[test_bookmarks_restore_notification.js]
+[test_bookmarks_setNullTitle.js]
+[test_broken_folderShortcut_result.js]
+[test_browserhistory.js]
+[test_bug636917_isLivemark.js]
+[test_childlessTags.js]
+[test_corrupt_telemetry.js]
+[test_crash_476292.js]
+[test_database_replaceOnStartup.js]
+[test_download_history.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_frecency.js]
+[test_frecency_zero_updated.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_getChildIndex.js]
+[test_getPlacesInfo.js]
+[test_history.js]
+[test_history_autocomplete_tags.js]
+[test_history_catobs.js]
+[test_history_clear.js]
+[test_history_notifications.js]
+[test_history_observer.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_history_sidebar.js]
+[test_hosts_triggers.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_mozIAsyncLivemarks.js]
+[test_multi_queries.js]
+# Bug 676989: test fails consistently on Android
+fail-if = os == "android"
+[test_multi_word_tags.js]
+[test_nsINavHistoryViewer.js]
+# Bug 902248: intermittent timeouts on all platforms
+skip-if = true
+[test_null_interfaces.js]
+[test_onItemChanged_tags.js]
+[test_pageGuid_bookmarkGuid.js]
+[test_frecency_observers.js]
+[test_placeURIs.js]
+[test_PlacesSearchAutocompleteProvider.js]
+[test_PlacesUtils_asyncGetBookmarkIds.js]
+[test_PlacesUtils_invalidateCachedGuidFor.js]
+[test_PlacesUtils_lazyobservers.js]
+[test_placesTxn.js]
+[test_preventive_maintenance.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_preventive_maintenance_checkAndFixDatabase.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_preventive_maintenance_runTasks.js]
+[test_promiseBookmarksTree.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_resolveNullBookmarkTitles.js]
+[test_result_sort.js]
+[test_resultsAsVisit_details.js]
+[test_sql_guid_functions.js]
+[test_svg_favicon.js]
+[test_sync_utils.js]
+[test_tag_autocomplete_search.js]
+[test_tagging.js]
+[test_telemetry.js]
+[test_update_frecency_after_delete.js]
+# Bug 676989: test hangs consistently on Android
+skip-if = os == "android"
+[test_utils_backups_create.js]
+[test_utils_getURLsForContainerNode.js]
+[test_utils_setAnnotationsFor.js]
+[test_visitsInDB.js]