summaryrefslogtreecommitdiffstats
path: root/browser/components/places/tests
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places/tests')
-rw-r--r--browser/components/places/tests/browser/bookmark_dummy_1.html9
-rw-r--r--browser/components/places/tests/browser/bookmark_dummy_2.html9
-rw-r--r--browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html9
-rw-r--r--browser/components/places/tests/browser/browser.toml236
-rw-r--r--browser/components/places/tests/browser/browser_addBookmarkForFrame.js150
-rw-r--r--browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js165
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js49
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js68
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js188
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js66
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js126
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js78
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js140
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js221
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js110
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js89
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js67
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js221
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js106
-rw-r--r--browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js47
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_add_tags.js228
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_all_tabs.js46
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_backup_export_import.js205
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_change_location.js213
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js798
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js172
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_folder_moveability.js139
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js43
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_popup.js712
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_private_window.js58
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_remove_tags.js256
-rw-r--r--browser/components/places/tests/browser/browser_bookmark_titles.js129
-rw-r--r--browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js79
-rw-r--r--browser/components/places/tests/browser/browser_bookmarksProperties.js532
-rw-r--r--browser/components/places/tests/browser/browser_bookmarks_change_title.js256
-rw-r--r--browser/components/places/tests/browser/browser_bookmarks_change_url.js106
-rw-r--r--browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js213
-rw-r--r--browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js87
-rw-r--r--browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js162
-rw-r--r--browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js50
-rw-r--r--browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js78
-rw-r--r--browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js162
-rw-r--r--browser/components/places/tests/browser/browser_check_correct_controllers.js114
-rw-r--r--browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js234
-rw-r--r--browser/components/places/tests/browser/browser_controller_onDrop.js125
-rw-r--r--browser/components/places/tests/browser/browser_controller_onDrop_query.js132
-rw-r--r--browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js312
-rw-r--r--browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js125
-rw-r--r--browser/components/places/tests/browser/browser_copy_query_without_tree.js113
-rw-r--r--browser/components/places/tests/browser/browser_cutting_bookmarks.js87
-rw-r--r--browser/components/places/tests/browser/browser_default_bookmark_location.js274
-rw-r--r--browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js255
-rw-r--r--browser/components/places/tests/browser/browser_drag_folder_on_newTab.js100
-rw-r--r--browser/components/places/tests/browser/browser_editBookmark_keywords.js64
-rw-r--r--browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js57
-rw-r--r--browser/components/places/tests/browser/browser_forgetthissite.js262
-rw-r--r--browser/components/places/tests/browser/browser_history_sidebar_search.js71
-rw-r--r--browser/components/places/tests/browser/browser_import_button.js187
-rw-r--r--browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js124
-rw-r--r--browser/components/places/tests/browser/browser_library_bookmark_pages.js101
-rw-r--r--browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js146
-rw-r--r--browser/components/places/tests/browser/browser_library_commands.js335
-rw-r--r--browser/components/places/tests/browser/browser_library_delete.js157
-rw-r--r--browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js111
-rw-r--r--browser/components/places/tests/browser/browser_library_delete_tags.js60
-rw-r--r--browser/components/places/tests/browser/browser_library_downloads.js65
-rw-r--r--browser/components/places/tests/browser/browser_library_left_pane_middleclick.js106
-rw-r--r--browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js52
-rw-r--r--browser/components/places/tests/browser/browser_library_middleclick.js234
-rw-r--r--browser/components/places/tests/browser/browser_library_new_bookmark.js95
-rw-r--r--browser/components/places/tests/browser/browser_library_openFlatContainer.js125
-rw-r--r--browser/components/places/tests/browser/browser_library_open_all.js57
-rw-r--r--browser/components/places/tests/browser/browser_library_open_all_with_separator.js61
-rw-r--r--browser/components/places/tests/browser/browser_library_open_bookmark.js46
-rw-r--r--browser/components/places/tests/browser/browser_library_open_leak.js22
-rw-r--r--browser/components/places/tests/browser/browser_library_panel_leak.js71
-rw-r--r--browser/components/places/tests/browser/browser_library_sameNodeDetailsPaneOptimization.js45
-rw-r--r--browser/components/places/tests/browser/browser_library_search.js206
-rw-r--r--browser/components/places/tests/browser/browser_library_tags_visibility.js88
-rw-r--r--browser/components/places/tests/browser/browser_library_telemetry.js413
-rw-r--r--browser/components/places/tests/browser/browser_library_tree_leak.js24
-rw-r--r--browser/components/places/tests/browser/browser_library_views_liveupdate.js217
-rw-r--r--browser/components/places/tests/browser/browser_library_warnOnOpen.js159
-rw-r--r--browser/components/places/tests/browser/browser_markPageAsFollowedLink.js75
-rw-r--r--browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js58
-rw-r--r--browser/components/places/tests/browser/browser_paste_bookmarks.js439
-rw-r--r--browser/components/places/tests/browser/browser_paste_into_tags.js116
-rw-r--r--browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js99
-rw-r--r--browser/components/places/tests/browser/browser_remove_bookmarks.js155
-rw-r--r--browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js141
-rw-r--r--browser/components/places/tests/browser/browser_sidebar_history_telemetry.js285
-rw-r--r--browser/components/places/tests/browser/browser_sidebar_on_customization.js100
-rw-r--r--browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js134
-rw-r--r--browser/components/places/tests/browser/browser_sidebarpanels_click.js172
-rw-r--r--browser/components/places/tests/browser/browser_sort_in_library.js248
-rw-r--r--browser/components/places/tests/browser/browser_stayopenmenu.js267
-rw-r--r--browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js200
-rw-r--r--browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js69
-rw-r--r--browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js50
-rw-r--r--browser/components/places/tests/browser/browser_toolbar_drop_text.js136
-rw-r--r--browser/components/places/tests/browser/browser_toolbar_library_open_recent.js158
-rw-r--r--browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js601
-rw-r--r--browser/components/places/tests/browser/browser_toolbar_overflow.js436
-rw-r--r--browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js72
-rw-r--r--browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js92
-rw-r--r--browser/components/places/tests/browser/browser_views_iconsupdate.js132
-rw-r--r--browser/components/places/tests/browser/browser_views_liveupdate.js493
-rw-r--r--browser/components/places/tests/browser/favicon-normal16.pngbin0 -> 286 bytes
-rw-r--r--browser/components/places/tests/browser/frameLeft.html8
-rw-r--r--browser/components/places/tests/browser/frameRight.html8
-rw-r--r--browser/components/places/tests/browser/framedPage.html9
-rw-r--r--browser/components/places/tests/browser/head.js570
-rw-r--r--browser/components/places/tests/browser/interactions/browser.toml36
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js105
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_clearHistory.js106
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_disabledHistory.js102
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_referrer.js45
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js162
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js208
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_typing.js410
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js171
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_view_time.js410
-rw-r--r--browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js123
-rw-r--r--browser/components/places/tests/browser/interactions/head.js201
-rw-r--r--browser/components/places/tests/browser/interactions/scrolling.html121
-rw-r--r--browser/components/places/tests/browser/interactions/scrolling_subframe.html25
-rw-r--r--browser/components/places/tests/browser/keyword_form.html17
-rw-r--r--browser/components/places/tests/browser/pageopeningwindow.html10
-rw-r--r--browser/components/places/tests/browser/sidebarpanels_click_test_page.html7
-rw-r--r--browser/components/places/tests/chrome/chrome.toml14
-rw-r--r--browser/components/places/tests/chrome/head.js36
-rw-r--r--browser/components/places/tests/chrome/test_0_bug510634.xhtml100
-rw-r--r--browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml88
-rw-r--r--browser/components/places/tests/chrome/test_bug549192.xhtml130
-rw-r--r--browser/components/places/tests/chrome/test_bug549491.xhtml78
-rw-r--r--browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml85
-rw-r--r--browser/components/places/tests/chrome/test_treeview_date.xhtml159
-rw-r--r--browser/components/places/tests/marionette/manifest.toml4
-rw-r--r--browser/components/places/tests/marionette/test_reopen_from_library.py164
-rw-r--r--browser/components/places/tests/unit/bookmarks.glue.html16
-rw-r--r--browser/components/places/tests/unit/bookmarks.glue.json83
-rw-r--r--browser/components/places/tests/unit/corruptDB.sqlitebin0 -> 32772 bytes
-rw-r--r--browser/components/places/tests/unit/distribution.ini30
-rw-r--r--browser/components/places/tests/unit/head_bookmarks.js82
-rw-r--r--browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js107
-rw-r--r--browser/components/places/tests/unit/test_PUIU_setCharsetForPage.js141
-rw-r--r--browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js81
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js33
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt.js53
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js47
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js50
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_distribution.js143
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_migrate.js68
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_prefs.js161
-rw-r--r--browser/components/places/tests/unit/test_browserGlue_restore.js55
-rw-r--r--browser/components/places/tests/unit/test_clearHistory_shutdown.js183
-rw-r--r--browser/components/places/tests/unit/test_interactions_blocklist.js67
-rw-r--r--browser/components/places/tests/unit/test_invalid_defaultLocation.js24
-rw-r--r--browser/components/places/tests/unit/xpcshell.toml38
159 files changed, 22412 insertions, 0 deletions
diff --git a/browser/components/places/tests/browser/bookmark_dummy_1.html b/browser/components/places/tests/browser/bookmark_dummy_1.html
new file mode 100644
index 0000000000..c03e0c18c0
--- /dev/null
+++ b/browser/components/places/tests/browser/bookmark_dummy_1.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Bookmark Dummy 1</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Bookmark Dummy 1</p>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/bookmark_dummy_2.html b/browser/components/places/tests/browser/bookmark_dummy_2.html
new file mode 100644
index 0000000000..229a730b32
--- /dev/null
+++ b/browser/components/places/tests/browser/bookmark_dummy_2.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Bookmark Dummy 2</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Bookmark Dummy 2</p>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html b/browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html
new file mode 100644
index 0000000000..54a87d3247
--- /dev/null
+++ b/browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>Bookmarklet windowOpen Dummy</title>
+<meta http-equiv="Content-Type" content="text/html;charset=utf-8"></meta>
+</head>
+<body>
+<p>Bookmarklet windowOpen Dummy</p>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/browser.toml b/browser/components/places/tests/browser/browser.toml
new file mode 100644
index 0000000000..1b0e2571d4
--- /dev/null
+++ b/browser/components/places/tests/browser/browser.toml
@@ -0,0 +1,236 @@
+# 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/.
+
+[DEFAULT]
+support-files = [
+ "head.js",
+ "framedPage.html",
+ "frameLeft.html",
+ "frameRight.html",
+ "sidebarpanels_click_test_page.html",
+ "keyword_form.html",
+]
+
+["browser_addBookmarkForFrame.js"]
+
+["browser_autoshow_bookmarks_toolbar.js"]
+
+["browser_bookmarkMenu_hiddenWindow.js"]
+skip-if = ["os != 'mac'"] # Mac-only functionality
+
+["browser_bookmarkProperties_addFolderDefaultButton.js"]
+
+["browser_bookmarkProperties_addKeywordForThisSearch.js"]
+
+["browser_bookmarkProperties_bookmarkAllTabs.js"]
+
+["browser_bookmarkProperties_cancel.js"]
+
+["browser_bookmarkProperties_editFolder.js"]
+
+["browser_bookmarkProperties_editTagContainer.js"]
+
+["browser_bookmarkProperties_folderSelection.js"]
+
+["browser_bookmarkProperties_newFolder.js"]
+
+["browser_bookmarkProperties_no_user_actions.js"]
+
+["browser_bookmarkProperties_readOnlyRoot.js"]
+
+["browser_bookmarkProperties_remember_folders.js"]
+
+["browser_bookmarkProperties_speculativeConnection.js"]
+
+["browser_bookmarkProperties_xulStore.js"]
+
+["browser_bookmark_add_tags.js"]
+https_first_disabled = true
+
+["browser_bookmark_all_tabs.js"]
+https_first_disabled = true
+support-files = [
+ "bookmark_dummy_1.html",
+ "bookmark_dummy_2.html",
+]
+
+["browser_bookmark_backup_export_import.js"]
+
+["browser_bookmark_change_location.js"]
+
+["browser_bookmark_context_menu_contents.js"]
+
+["browser_bookmark_copy_folder_tree.js"]
+
+["browser_bookmark_folder_moveability.js"]
+
+["browser_bookmark_menu_ctrl_click.js"]
+
+["browser_bookmark_popup.js"]
+skip-if = ["verify && os == 'win'"]
+
+["browser_bookmark_private_window.js"]
+
+["browser_bookmark_remove_tags.js"]
+
+["browser_bookmark_titles.js"]
+https_first_disabled = true
+support-files = ["../../../../base/content/test/general/dummy_page.html"]
+
+["browser_bookmarklet_windowOpen.js"]
+support-files = ["bookmarklet_windowOpen_dummy.html"]
+
+["browser_bookmarksProperties.js"]
+
+["browser_bookmarks_change_title.js"]
+
+["browser_bookmarks_change_url.js"]
+
+["browser_bookmarks_sidebar_search.js"]
+support-files = ["pageopeningwindow.html"]
+
+["browser_bookmarks_toolbar_context_menu_view_options.js"]
+
+["browser_bookmarks_toolbar_telemetry.js"]
+
+["browser_bug427633_no_newfolder_if_noip.js"]
+
+["browser_bug485100-change-case-loses-tag.js"]
+
+["browser_bug631374_tags_selector_scroll.js"]
+support-files = ["favicon-normal16.png"]
+
+["browser_check_correct_controllers.js"]
+
+["browser_click_bookmarks_on_toolbar.js"]
+https_first_disabled = true
+
+["browser_controller_onDrop.js"]
+
+["browser_controller_onDrop_query.js"]
+
+["browser_controller_onDrop_sidebar.js"]
+
+["browser_controller_onDrop_tagFolder.js"]
+
+["browser_copy_query_without_tree.js"]
+
+["browser_cutting_bookmarks.js"]
+
+["browser_default_bookmark_location.js"]
+
+["browser_drag_bookmarks_on_toolbar.js"]
+
+["browser_drag_folder_on_newTab.js"]
+https_first_disabled = true
+
+["browser_editBookmark_keywords.js"]
+
+["browser_enable_toolbar_sidebar.js"]
+skip-if = ["verify && debug && os == 'win'"]
+
+["browser_forgetthissite.js"]
+
+["browser_history_sidebar_search.js"]
+
+["browser_import_button.js"]
+
+["browser_library_bookmark_clear_visits.js"]
+
+["browser_library_bookmark_pages.js"]
+
+["browser_library_bulk_tag_bookmarks.js"]
+
+["browser_library_commands.js"]
+
+["browser_library_delete.js"]
+
+["browser_library_delete_bookmarks_in_tags.js"]
+
+["browser_library_delete_tags.js"]
+
+["browser_library_downloads.js"]
+
+["browser_library_left_pane_middleclick.js"]
+
+["browser_library_left_pane_select_hierarchy.js"]
+
+["browser_library_middleclick.js"]
+
+["browser_library_new_bookmark.js"]
+
+["browser_library_openFlatContainer.js"]
+
+["browser_library_open_all.js"]
+
+["browser_library_open_all_with_separator.js"]
+
+["browser_library_open_bookmark.js"]
+
+["browser_library_open_leak.js"]
+
+["browser_library_panel_leak.js"]
+
+["browser_library_sameNodeDetailsPaneOptimization.js"]
+
+["browser_library_search.js"]
+
+["browser_library_tags_visibility.js"]
+
+["browser_library_telemetry.js"]
+
+["browser_library_tree_leak.js"]
+
+["browser_library_views_liveupdate.js"]
+
+["browser_library_warnOnOpen.js"]
+
+["browser_markPageAsFollowedLink.js"]
+
+["browser_panelview_bookmarks_delete.js"]
+
+["browser_paste_bookmarks.js"]
+
+["browser_paste_into_tags.js"]
+
+["browser_paste_resets_cut_highlights.js"]
+
+["browser_remove_bookmarks.js"]
+
+["browser_sidebar_bookmarks_telemetry.js"]
+
+["browser_sidebar_history_telemetry.js"]
+
+["browser_sidebar_on_customization.js"]
+
+["browser_sidebar_open_bookmarks.js"]
+
+["browser_sidebarpanels_click.js"]
+
+["browser_sort_in_library.js"]
+
+["browser_stayopenmenu.js"]
+
+["browser_toolbar_drop_bookmarklet.js"]
+
+["browser_toolbar_drop_multiple_flavors.js"]
+
+["browser_toolbar_drop_multiple_with_bookmarklet.js"]
+
+["browser_toolbar_drop_text.js"]
+
+["browser_toolbar_library_open_recent.js"]
+https_first_disabled = true
+
+["browser_toolbar_other_bookmarks.js"]
+
+["browser_toolbar_overflow.js"]
+
+["browser_toolbarbutton_menu_context.js"]
+
+["browser_toolbarbutton_menu_show_in_folder.js"]
+
+["browser_views_iconsupdate.js"]
+
+["browser_views_liveupdate.js"]
diff --git a/browser/components/places/tests/browser/browser_addBookmarkForFrame.js b/browser/components/places/tests/browser/browser_addBookmarkForFrame.js
new file mode 100644
index 0000000000..44a7f5e17a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_addBookmarkForFrame.js
@@ -0,0 +1,150 @@
+/**
+ * Tests that the add bookmark for frame dialog functions correctly.
+ */
+
+const BASE_URL =
+ "http://mochi.test:8888/browser/browser/components/places/tests/browser";
+const PAGE_URL = BASE_URL + "/framedPage.html";
+const LEFT_URL = BASE_URL + "/frameLeft.html";
+const RIGHT_URL = BASE_URL + "/frameRight.html";
+
+function activateBookmarkFrame(contentAreaContextMenu) {
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ return async function () {
+ let frameMenuItem = document.getElementById("frame");
+ let frameMenu = frameMenuItem.querySelector(":scope > menupopup");
+ let frameMenuShown = BrowserTestUtils.waitForEvent(frameMenu, "popupshown");
+ frameMenuItem.openMenu(true);
+ await frameMenuShown;
+ let bookmarkFrame = document.getElementById("context-bookmarkframe");
+ frameMenu.activateItem(bookmarkFrame);
+ await popupHiddenPromise;
+ };
+}
+
+async function withAddBookmarkForFrame(taskFn) {
+ // Open a tab and wait for all the subframes to load.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#left",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ await withBookmarksDialog(
+ true,
+ activateBookmarkFrame(contentAreaContextMenu),
+ taskFn
+ );
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_open_add_bookmark_for_frame() {
+ info("Test basic opening of the add bookmark for frame dialog.");
+ await withAddBookmarkForFrame(async dialogWin => {
+ let namepicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+ Assert.ok(!namepicker.readOnly, "Name field is writable");
+ Assert.equal(namepicker.value, "Left frame", "Name field is correct.");
+
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ let expectedFolderName = PlacesUtils.getString(expectedFolder);
+
+ let folderPicker = dialogWin.document.getElementById(
+ "editBMPanel_folderMenuList"
+ );
+
+ await TestUtils.waitForCondition(
+ () => folderPicker.selectedItem.label == expectedFolderName,
+ "Dialog: The folder is the expected one."
+ );
+
+ let tagsField = dialogWin.document.getElementById("editBMPanel_tagsField");
+ Assert.equal(tagsField.value, "", "Dialog: The tags field should be empty");
+ });
+});
+
+add_task(async function test_move_bookmark_whilst_add_bookmark_open() {
+ info(
+ "EditBookmark: Test moving a bookmark whilst the add bookmark for frame dialog is open."
+ );
+ await PlacesUtils.bookmarks.eraseEverything();
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#left",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ await withBookmarksDialog(
+ false,
+ activateBookmarkFrame(contentAreaContextMenu),
+ async function (dialogWin) {
+ let expectedGuid = await PlacesUIUtils.defaultParentGuid;
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ let expectedFolderName = PlacesUtils.getString(expectedFolder);
+
+ let folderPicker = dialogWin.document.getElementById(
+ "editBMPanel_folderMenuList"
+ );
+
+ Assert.equal(
+ folderPicker.selectedItem.label,
+ expectedFolderName,
+ "EditBookmark: The folder is the expected one."
+ );
+
+ Assert.equal(
+ folderPicker.getAttribute("selectedGuid"),
+ expectedGuid,
+ "EditBookmark: Should have the correct default guid selected"
+ );
+
+ dialogWin.document.getElementById("editBMPanel_foldersExpander").click();
+ let folderTree = dialogWin.document.getElementById(
+ "editBMPanel_folderTree"
+ );
+ folderTree.selectItems([PlacesUtils.bookmarks.menuGuid]);
+ folderTree.blur();
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ }
+ );
+ let url = makeURI(LEFT_URL);
+ // Check the bookmark has been moved as expected.
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url });
+
+ Assert.equal(
+ bookmark.parentGuid,
+ PlacesUtils.bookmarks.menuGuid,
+ "EditBookmark: The bookmark should be moved to the expected folder."
+ );
+
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js b/browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js
new file mode 100644
index 0000000000..c841eb276b
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const LOCATION_PREF = "browser.bookmarks.defaultLocation";
+const TOOLBAR_VISIBILITY_PREF = "browser.toolbars.bookmarks.visibility";
+let bookmarkPanel;
+let win;
+
+add_setup(async function () {
+ Services.prefs.clearUserPref(LOCATION_PREF);
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let oldTimeout = win.StarUI._autoCloseTimeout;
+ // Make the timeout something big, so it doesn't interact badly with tests.
+ win.StarUI._autoCloseTimeout = 6000000;
+
+ win.StarUI._createPanelIfNeeded();
+ bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+ bookmarkPanel.setAttribute("animate", false);
+
+ registerCleanupFunction(async () => {
+ bookmarkPanel = null;
+ win.StarUI._autoCloseTimeout = oldTimeout;
+ await BrowserTestUtils.closeWindow(win);
+ win = null;
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.prefs.clearUserPref(LOCATION_PREF);
+ });
+});
+
+/**
+ * Helper to check we've shown the toolbar
+ *
+ * @param {object} options
+ * Options for the test
+ * @param {boolean} options.showToolbar
+ * If the toolbar should be shown or not
+ * @param {string} options.expectedFolder
+ * The expected folder to be shown
+ * @param {string} options.reason
+ * The reason the toolbar should be shown
+ */
+async function checkResponse({ showToolbar, expectedFolder, reason }) {
+ // Check folder.
+ let menuList = win.document.getElementById("editBMPanel_folderMenuList");
+
+ Assert.equal(
+ menuList.label,
+ PlacesUtils.getString(expectedFolder),
+ `Should have ${expectedFolder} selected ${reason}.`
+ );
+
+ // Check toolbar:
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ Assert.equal(
+ !toolbar.collapsed,
+ showToolbar,
+ `Toolbar should be ${showToolbar ? "visible" : "hidden"} ${reason}.`
+ );
+
+ // Confirm and close the dialog.
+ let hiddenPromise = promisePopupHidden(
+ win.document.getElementById("editBookmarkPanel")
+ );
+ win.document.getElementById("editBookmarkPanelRemoveButton").click();
+ await hiddenPromise;
+}
+
+/**
+ * Test that if we create a bookmark on the toolbar, we show the
+ * toolbar:
+ */
+add_task(async function test_new_on_toolbar() {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com/1" },
+ async browser => {
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ Assert.equal(
+ toolbar.collapsed,
+ true,
+ "Bookmarks toolbar should start out collapsed."
+ );
+ let shownPromise = promisePopupShown(
+ win.document.getElementById("editBookmarkPanel")
+ );
+ win.document.getElementById("Browser:AddBookmarkAs").doCommand();
+ await shownPromise;
+ await TestUtils.waitForCondition(
+ () => !toolbar.collapsed,
+ "Toolbar should be shown."
+ );
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ let reason = "when creating a bookmark there";
+ await checkResponse({ showToolbar: true, expectedFolder, reason });
+ }
+ );
+});
+
+/**
+ * Test that if we create a bookmark on the toolbar, we do not
+ * show the toolbar if toolbar should never be shown:
+ */
+add_task(async function test_new_on_toolbar_never_show_toolbar() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[TOOLBAR_VISIBILITY_PREF, "never"]],
+ });
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com/1" },
+ async browser => {
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ Assert.equal(
+ toolbar.collapsed,
+ true,
+ "Bookmarks toolbar should start out collapsed."
+ );
+ let shownPromise = promisePopupShown(
+ win.document.getElementById("editBookmarkPanel")
+ );
+ win.document.getElementById("Browser:AddBookmarkAs").doCommand();
+ await shownPromise;
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ let reason = "when the visibility pref is 'never'";
+ await checkResponse({ showToolbar: false, expectedFolder, reason });
+ }
+ );
+
+ await SpecialPowers.popPrefEnv();
+});
+
+/**
+ * Test that if we edit an existing bookmark, we don't show the toolbar.
+ */
+add_task(async function test_existing_on_toolbar() {
+ // Create the bookmark first:
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "Test for editing",
+ url: "https://example.com/editing-test",
+ });
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: "https://example.com/editing-test" },
+ async browser => {
+ await TestUtils.waitForCondition(
+ () => win.BookmarkingUI.status == BookmarkingUI.STATUS_STARRED,
+ "Page should be starred."
+ );
+
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ Assert.equal(
+ toolbar.collapsed,
+ true,
+ "Bookmarks toolbar should start out collapsed."
+ );
+ await clickBookmarkStar(win);
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ let reason = "when editing a bookmark there";
+ await checkResponse({ showToolbar: false, expectedFolder, reason });
+ }
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js b/browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js
new file mode 100644
index 0000000000..4502b65e69
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js
@@ -0,0 +1,49 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://example.com/",
+ title: "Test1",
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_menu_in_hidden_window() {
+ let hwDoc = Services.appShell.hiddenDOMWindow.document;
+ let bmPopup = hwDoc.getElementById("bookmarksMenuPopup");
+ var popupEvent = hwDoc.createEvent("MouseEvent");
+ popupEvent.initMouseEvent(
+ "popupshowing",
+ true,
+ true,
+ Services.appShell.hiddenDOMWindow,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null
+ );
+ bmPopup.dispatchEvent(popupEvent);
+
+ let testMenuitem = [...bmPopup.children].find(
+ node => node.getAttribute("label") == "Test1"
+ );
+ Assert.ok(
+ testMenuitem,
+ "Should have found the test bookmark in the hidden window bookmark menu"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js
new file mode 100644
index 0000000000..0c04dbd243
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js
@@ -0,0 +1,68 @@
+"use strict";
+
+add_task(async function add_folder_default_button() {
+ info(
+ "Bug 475529 - Add is the default button for the new folder dialog + " +
+ "Bug 1206376 - Changing properties of a new bookmark while adding it " +
+ "acts on the last bookmark in the current container"
+ );
+
+ // Add a new bookmark at index 0 in the unfiled folder.
+ let insertionIndex = 0;
+ let newBookmark = await PlacesUtils.bookmarks.insert({
+ index: insertionIndex,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "http://example.com/",
+ });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ // Select the new bookmark in the sidebar.
+ tree.selectItems([newBookmark.guid]);
+ Assert.ok(
+ tree.controller.isCommandEnabled("placesCmd_new:folder"),
+ "'placesCmd_new:folder' on current selected node is enabled"
+ );
+
+ // Create a new folder. Since the new bookmark is selected, and new items
+ // are inserted at the index of the currently selected item, the new folder
+ // will be inserted at index 0.
+ await withBookmarksDialog(
+ false,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_new:folder");
+ },
+ async function test(dialogWin) {
+ const notifications = [
+ PlacesTestUtils.waitForNotification("bookmark-added", events =>
+ events.some(e => e.title === "n")
+ ),
+ PlacesTestUtils.waitForNotification("bookmark-moved", null),
+ ];
+
+ fillBookmarkTextField("editBMPanel_namePicker", "n", dialogWin, false);
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ await Promise.all(notifications);
+
+ let newFolder = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: insertionIndex,
+ });
+
+ is(newFolder.title, "n", "folder name has been edited");
+
+ let bm = await PlacesUtils.bookmarks.fetch(newBookmark.guid);
+ Assert.equal(
+ bm.index,
+ insertionIndex + 1,
+ "Bookmark should have been shifted to the next index"
+ );
+
+ await PlacesUtils.bookmarks.remove(newFolder);
+ await PlacesUtils.bookmarks.remove(newBookmark);
+ }
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js
new file mode 100644
index 0000000000..514519810a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js
@@ -0,0 +1,188 @@
+"use strict";
+
+const TEST_URL =
+ "http://mochi.test:8888/browser/browser/components/places/tests/browser/keyword_form.html";
+
+let contentAreaContextMenu = document.getElementById("contentAreaContextMenu");
+
+add_task(async function add_keyword() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL,
+ },
+ async function (browser) {
+ // We must wait for the context menu code to build metadata.
+ await openContextMenuForContentSelector(
+ browser,
+ '#form1 > input[name="search"]'
+ );
+
+ await withBookmarksDialog(
+ false,
+ function () {
+ AddKeywordForSearchField();
+ contentAreaContextMenu.hidePopup();
+ },
+ async function (dialogWin) {
+ let acceptBtn = dialogWin.document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ Assert.ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let promiseKeywordNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-keyword-changed",
+ events => events.some(event => event.keyword === "kw")
+ );
+
+ fillBookmarkTextField("editBMPanel_keywordField", "kw", dialogWin);
+
+ Assert.ok(!acceptBtn.disabled, "Accept button is enabled");
+
+ acceptBtn.click();
+ await promiseKeywordNotification;
+
+ // After the notification, the keywords cache will update asynchronously.
+ info("Check the keyword entry has been created");
+ let entry;
+ await TestUtils.waitForCondition(async function () {
+ entry = await PlacesUtils.keywords.fetch("kw");
+ return !!entry;
+ }, "Unable to find the expected keyword");
+ Assert.equal(entry.keyword, "kw", "keyword is correct");
+ Assert.equal(entry.url.href, TEST_URL, "URL is correct");
+ Assert.equal(
+ entry.postData,
+ "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s",
+ "POST data is correct"
+ );
+ let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL });
+ Assert.equal(
+ bm.parentGuid,
+ await PlacesUIUtils.defaultParentGuid,
+ "Should have created the keyword in the right folder."
+ );
+
+ info("Check the charset has been saved");
+ let pageInfo = await PlacesUtils.history.fetch(TEST_URL, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ "windows-1252",
+ "charset is correct"
+ );
+
+ // Now check getShortcutOrURI.
+ let data = await UrlbarUtils.getShortcutOrURIAndPostData("kw test");
+ Assert.equal(
+ getPostDataString(data.postData),
+ "accenti=\u00E0\u00E8\u00EC\u00F2\u00F9&search=test",
+ "getShortcutOrURI POST data is correct"
+ );
+ Assert.equal(data.url, TEST_URL, "getShortcutOrURI URL is correct");
+ },
+ () => PlacesUtils.bookmarks.eraseEverything()
+ );
+ }
+ );
+});
+
+add_task(async function reopen_same_field() {
+ await PlacesUtils.keywords.insert({
+ url: TEST_URL,
+ keyword: "kw",
+ postData: "accenti%3D%E0%E8%EC%F2%F9&search%3D%25s",
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.keywords.remove("kw");
+ });
+ // Reopening on the same input field should show the existing keyword.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL,
+ },
+ async function (browser) {
+ // We must wait for the context menu code to build metadata.
+ await openContextMenuForContentSelector(
+ browser,
+ '#form1 > input[name="search"]'
+ );
+
+ await withBookmarksDialog(
+ true,
+ function () {
+ AddKeywordForSearchField();
+ contentAreaContextMenu.hidePopup();
+ },
+ async function (dialogWin) {
+ let elt = dialogWin.document.getElementById(
+ "editBMPanel_keywordField"
+ );
+ Assert.equal(elt.value, "kw", "Keyword should be the previous value");
+
+ let acceptBtn = dialogWin.document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ ok(!acceptBtn.disabled, "Accept button is enabled");
+ },
+ () => PlacesUtils.bookmarks.eraseEverything()
+ );
+ }
+ );
+});
+
+add_task(async function open_other_field() {
+ await PlacesUtils.keywords.insert({
+ url: TEST_URL,
+ keyword: "kw2",
+ postData: "search%3D%25s",
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.keywords.remove("kw2");
+ });
+ // Reopening on another field of the same page that has different postData
+ // should not show the existing keyword.
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: TEST_URL,
+ },
+ async function (browser) {
+ // We must wait for the context menu code to build metadata.
+ await openContextMenuForContentSelector(
+ browser,
+ '#form2 > input[name="search"]'
+ );
+
+ await withBookmarksDialog(
+ true,
+ function () {
+ AddKeywordForSearchField();
+ contentAreaContextMenu.hidePopup();
+ },
+ function (dialogWin) {
+ let acceptBtn = dialogWin.document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ ok(acceptBtn.disabled, "Accept button is disabled");
+
+ let elt = dialogWin.document.getElementById(
+ "editBMPanel_keywordField"
+ );
+ is(elt.value, "");
+ },
+ () => PlacesUtils.bookmarks.eraseEverything()
+ );
+ }
+ );
+});
+
+function getPostDataString(stream) {
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(stream);
+ return sis.read(stream.available()).split("\n").pop();
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js b/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js
new file mode 100644
index 0000000000..805f9464e3
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js
@@ -0,0 +1,66 @@
+"use strict";
+
+const TEST_URLS = ["about:robots", "about:mozilla"];
+
+add_task(async function bookmark_all_tabs() {
+ let tabs = [];
+ for (let url of TEST_URLS) {
+ tabs.push(await BrowserTestUtils.openNewForegroundTab(gBrowser, url));
+ }
+ registerCleanupFunction(async function () {
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ await withBookmarksDialog(
+ false,
+ function open() {
+ document.getElementById("Browser:BookmarkAllTabs").doCommand();
+ },
+ async dialog => {
+ let acceptBtn = dialog.document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ Assert.ok(!acceptBtn.disabled, "Accept button is enabled");
+
+ let namepicker = dialog.document.getElementById("editBMPanel_namePicker");
+ Assert.ok(!namepicker.readOnly, "Name field is writable");
+ let folderName = dialog.document
+ .getElementById("stringBundle")
+ .getString("bookmarkAllTabsDefault");
+ Assert.equal(namepicker.value, folderName, "Name field is correct.");
+
+ let promiseBookmarkAdded =
+ PlacesTestUtils.waitForNotification("bookmark-added");
+
+ fillBookmarkTextField("editBMPanel_namePicker", "folder", dialog);
+
+ let folderPicker = dialog.document.getElementById(
+ "editBMPanel_folderMenuList"
+ );
+
+ let defaultParentGuid = await PlacesUIUtils.defaultParentGuid;
+ // Check the initial state of the folder picker.
+ await TestUtils.waitForCondition(
+ () => folderPicker.getAttribute("selectedGuid") == defaultParentGuid,
+ "The folder is the expected one."
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialog);
+ await promiseBookmarkAdded;
+ for (const url of TEST_URLS) {
+ const { parentGuid } = await PlacesUtils.bookmarks.fetch({ url });
+ const folder = await PlacesUtils.bookmarks.fetch({
+ guid: parentGuid,
+ });
+ is(
+ folder.title,
+ "folder",
+ "Should have created the bookmark in the right folder."
+ );
+ }
+ }
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js b/browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js
new file mode 100644
index 0000000000..5652358acb
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js
@@ -0,0 +1,126 @@
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const sandbox = sinon.createSandbox();
+
+registerCleanupFunction(async function () {
+ sandbox.restore();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+let bookmarks; // Bookmarks added via insertTree.
+
+add_setup(async function () {
+ bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "bm1",
+ url: "http://example.com",
+ },
+ {
+ title: "bm2",
+ url: "http://example.com/2",
+ },
+ ],
+ });
+
+ // Undo is called asynchronously - and not waited for. Since we're not
+ // expecting undo to be called, we can only tell this by stubbing it.
+ sandbox.stub(PlacesTransactions, "undo").returns(Promise.resolve());
+});
+
+// Tests for bug 1391393 - Ensures that if the user cancels the bookmark properties
+// dialog without having done any changes, then no undo is called.
+add_task(async function test_cancel_with_no_changes() {
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([bookmarks[0].guid]);
+
+ // Delete the bookmark to put something in the undo history.
+ // Rather than calling cmd_delete, we call the remove directly, so that we
+ // can await on it finishing, and be guaranteed that there's something
+ // in the history.
+ await tree.controller.remove("Remove Selection");
+
+ tree.selectItems([bookmarks[1].guid]);
+
+ // Now open the bookmarks dialog and cancel it.
+ await withBookmarksDialog(
+ true,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ let acceptButton = dialogWin.document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ await TestUtils.waitForCondition(
+ () => !acceptButton.disabled,
+ "The accept button should be enabled"
+ );
+ }
+ );
+
+ // Check the bookmark is still removed.
+ Assert.ok(
+ !(await PlacesUtils.bookmarks.fetch(bookmarks[0].guid)),
+ "The originally removed bookmark should not exist."
+ );
+
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch(bookmarks[1].guid),
+ "The second bookmark should still exist"
+ );
+
+ Assert.ok(
+ PlacesTransactions.undo.notCalled,
+ "undo should not have been called"
+ );
+ });
+});
+
+add_task(async function test_cancel_with_changes() {
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([bookmarks[1].guid]);
+
+ // Now open the bookmarks dialog and cancel it.
+ await withBookmarksDialog(
+ true,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ let acceptButton = dialogWin.document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ await TestUtils.waitForCondition(
+ () => !acceptButton.disabled,
+ "EditBookmark: The accept button should be enabled"
+ );
+
+ let namePicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+ fillBookmarkTextField("editBMPanel_namePicker", "new_n", dialogWin);
+
+ // Ensure that value in field has changed
+ Assert.equal(
+ namePicker.value,
+ "new_n",
+ "EditBookmark: The title is the expected one."
+ );
+ }
+ );
+
+ let oldBookmark = await PlacesUtils.bookmarks.fetch(bookmarks[1].guid);
+ Assert.equal(
+ oldBookmark.title,
+ "bm2",
+ "EditBookmark: The title hasn't been changed"
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js b/browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js
new file mode 100644
index 0000000000..fae9d01bec
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Tests the Properties dialog on a folder.
+
+add_task(async function test_bookmark_properties_dialog_on_folder() {
+ info("Bug 479348 - Properties on a root should be read-only.");
+
+ let bm = await PlacesUtils.bookmarks.insert({
+ title: "folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ // Select the new bookmark in the sidebar.
+ tree.selectItems([bm.guid]);
+ let folder = tree.selectedNode;
+ Assert.equal(folder.title, "folder", "Folder title is correct");
+ Assert.ok(
+ tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "'placesCmd_show:info' on folder is enabled"
+ );
+
+ await withBookmarksDialog(
+ false,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ // Check that the dialog is not read-only.
+ Assert.ok(
+ !dialogWin.gEditItemOverlay.readOnly,
+ "EditBookmark: Dialog should not be read-only"
+ );
+
+ // Check that name picker is not read only
+ let namepicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+ Assert.ok(
+ !namepicker.readOnly,
+ "EditBookmark: Name field should not be read-only"
+ );
+ Assert.equal(
+ namepicker.value,
+ "folder",
+ "EditBookmark:Node title is correct"
+ );
+
+ fillBookmarkTextField("editBMPanel_namePicker", "newname", dialogWin);
+ namepicker.blur();
+
+ Assert.equal(
+ namepicker.value,
+ "newname",
+ "EditBookmark: The title field has been changed"
+ );
+
+ // Confirm and close the dialog.
+ namepicker.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ // Ensure that the edit is finished before we hit cancel.
+ }
+ );
+
+ Assert.equal(
+ tree.selectedNode.title,
+ "newname",
+ "EditBookmark: The node has the correct title"
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
new file mode 100644
index 0000000000..f4e3b3e3fe
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js
@@ -0,0 +1,140 @@
+"use strict";
+
+add_task(async function editTagContainer() {
+ info("Bug 479348 - Properties on a root should be read-only.");
+ let uri = Services.io.newURI("http://example.com/");
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: uri.spec,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ PlacesUtils.tagging.tagURI(uri, ["tag1"]);
+
+ let library = await promiseLibrary();
+ let PlacesOrganizer = library.PlacesOrganizer;
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ });
+
+ PlacesOrganizer.selectLeftPaneBuiltIn("Tags");
+ let tree = PlacesOrganizer._places;
+ let tagsContainer = tree.selectedNode;
+ tagsContainer.containerOpen = true;
+ let fooTag = tagsContainer.getChild(0);
+ let tagNode = fooTag;
+ tree.selectNode(fooTag);
+ Assert.equal(tagNode.title, "tag1", "EditBookmark: tagNode title is correct");
+
+ Assert.ok(
+ tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "EditBookmark: 'placesCmd_show:info' on current selected node is enabled"
+ );
+
+ await withBookmarksDialog(
+ true,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ // Check that the dialog is not read-only.
+ Assert.ok(
+ !dialogWin.gEditItemOverlay.readOnly,
+ "EditBookmark: Dialog should not be read-only"
+ );
+
+ // Check that name picker is not read only
+ let namepicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+ Assert.ok(
+ !namepicker.readOnly,
+ "EditBookmark: Name field should not be read-only"
+ );
+ Assert.equal(
+ namepicker.value,
+ "tag1",
+ "EditBookmark: Node title is correct"
+ );
+
+ fillBookmarkTextField("editBMPanel_namePicker", "tag2", dialogWin);
+
+ // Although we have received the expected notifications, we need
+ // to let everything resolve to ensure the UI is updated.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ Assert.equal(
+ namepicker.value,
+ "tag2",
+ "EditBookmark: The title field has been changed"
+ );
+
+ // Try to set an empty title, it should restore the previous one.
+ fillBookmarkTextField("editBMPanel_namePicker", "", dialogWin);
+
+ Assert.equal(
+ namepicker.value,
+ "tag1",
+ "EditBookmark: The title field has been changed"
+ );
+ }
+ );
+
+ // Check the tag change hasn't changed
+ let tags = PlacesUtils.tagging.getTagsForURI(uri);
+ Assert.equal(tags.length, 1, "EditBookmark: Found the right number of tags");
+ Assert.deepEqual(
+ tags,
+ ["tag1"],
+ "EditBookmark: Found the expected unchanged tag"
+ );
+
+ await withBookmarksDialog(
+ false,
+ function openDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ // Check that the dialog is not read-only.
+ Assert.ok(
+ !dialogWin.gEditItemOverlay.readOnly,
+ "Dialog should not be read-only"
+ );
+
+ // Check that name picker is not read only
+ let namepicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+ Assert.ok(
+ !namepicker.readOnly,
+ "EditBookmark: Name field should not be read-only"
+ );
+ Assert.equal(
+ namepicker.value,
+ "tag1",
+ "EditBookmark: Node title is correct"
+ );
+
+ fillBookmarkTextField("editBMPanel_namePicker", "tag2", dialogWin);
+
+ Assert.equal(
+ namepicker.value,
+ "tag2",
+ "EditBookmark: The title field has been changed"
+ );
+ namepicker.blur();
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ }
+ );
+
+ tags = PlacesUtils.tagging.getTagsForURI(uri);
+ Assert.equal(tags.length, 1, "EditBookmark: Found the right number of tags");
+ Assert.deepEqual(
+ tags,
+ ["tag2"],
+ "EditBookmark: Found the expected Y changed tag"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js b/browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js
new file mode 100644
index 0000000000..39f3dd8822
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_URL = "about:robots";
+let bookmarkPanel;
+let folders;
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ Services.prefs.clearUserPref("browser.bookmarks.defaultLocation");
+
+ const tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ let oldTimeout = StarUI._autoCloseTimeout;
+ // Make the timeout something big, so it doesn't iteract badly with tests.
+ StarUI._autoCloseTimeout = 6000000;
+
+ StarUI._createPanelIfNeeded();
+ bookmarkPanel = document.getElementById("editBookmarkPanel");
+ bookmarkPanel.setAttribute("animate", false);
+
+ registerCleanupFunction(async () => {
+ bookmarkPanel = null;
+ StarUI._autoCloseTimeout = oldTimeout;
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_selectChoose() {
+ await clickBookmarkStar();
+
+ // Open folder selector.
+ let menuList = document.getElementById("editBMPanel_folderMenuList");
+ let folderTreeRow = document.getElementById("editBMPanel_folderTreeRow");
+
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ let expectedGuid = PlacesUtils.bookmarks.toolbarGuid;
+
+ Assert.equal(
+ menuList.label,
+ PlacesUtils.getString(expectedFolder),
+ "Should have the expected bookmarks folder selected by default"
+ );
+ Assert.equal(
+ menuList.getAttribute("selectedGuid"),
+ expectedGuid,
+ "Should have the correct default guid selected"
+ );
+ Assert.equal(
+ folderTreeRow.hidden,
+ true,
+ "Should have the folder tree hidden"
+ );
+
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ menuList.menupopup,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(menuList, {});
+ await promisePopup;
+
+ // Click the choose item.
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("editBMPanel_chooseFolderMenuItem"),
+ {}
+ );
+
+ await TestUtils.waitForCondition(
+ () => !folderTreeRow.hidden,
+ "Should show the folder tree"
+ );
+ let folderTree = document.getElementById("editBMPanel_folderTree");
+ Assert.ok(folderTree.view, "The view should have been connected");
+
+ Assert.equal(
+ menuList.getAttribute("selectedGuid"),
+ expectedGuid,
+ "Should still have the correct selected guid"
+ );
+ Assert.equal(
+ menuList.label,
+ PlacesUtils.getString(expectedFolder),
+ "Should have kept the same menu label"
+ );
+
+ let input = folderTree.shadowRoot.querySelector("input");
+
+ let newFolderButton = document.getElementById("editBMPanel_newFolderButton");
+ newFolderButton.click(); // This will start editing.
+
+ // Wait for editing:
+ await TestUtils.waitForCondition(() => !input.hidden);
+
+ // Click the arrow to collapse the list.
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("editBMPanel_foldersExpander"),
+ {}
+ );
+
+ await TestUtils.waitForCondition(
+ () => folderTreeRow.hidden,
+ "Should hide the folder tree"
+ );
+ ok(input.hidden, "Folder tree should not be broken.");
+
+ // Click the arrow to re-show the list.
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("editBMPanel_foldersExpander"),
+ {}
+ );
+
+ await TestUtils.waitForCondition(
+ () => !folderTreeRow.hidden,
+ "Should re-show the folder tree"
+ );
+ ok(input.hidden, "Folder tree should still not be broken.");
+
+ const promiseCancel = promisePopupHidden(
+ document.getElementById("editBookmarkPanel")
+ );
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ await promiseCancel;
+ Assert.ok(!folderTree.view, "The view should have been disconnected");
+});
+
+add_task(async function test_selectBookmarksMenu() {
+ await clickBookmarkStar();
+
+ // Open folder selector.
+ let menuList = document.getElementById("editBMPanel_folderMenuList");
+
+ const expectedFolder = "BookmarksMenuFolderTitle";
+ const expectedGuid = PlacesUtils.bookmarks.menuGuid;
+
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ menuList.menupopup,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(menuList, {});
+ await promisePopup;
+
+ // Click the bookmarks menu item.
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("editBMPanel_bmRootItem"),
+ {}
+ );
+
+ await TestUtils.waitForCondition(
+ () => menuList.getAttribute("selectedGuid") == expectedGuid,
+ "Should select the menu folder item"
+ );
+
+ Assert.equal(
+ menuList.label,
+ PlacesUtils.getString(expectedFolder),
+ "Should have updated the menu label"
+ );
+
+ // Click the arrow to show the folder tree.
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("editBMPanel_foldersExpander"),
+ {}
+ );
+ const folderTreeRow = document.getElementById("editBMPanel_folderTreeRow");
+ await BrowserTestUtils.waitForMutationCondition(
+ folderTreeRow,
+ { attributeFilter: ["hidden"] },
+ () => !folderTreeRow.hidden
+ );
+ const folderTree = document.getElementById("editBMPanel_folderTree");
+ Assert.equal(
+ folderTree.selectedNode.bookmarkGuid,
+ PlacesUtils.bookmarks.virtualMenuGuid,
+ "Folder tree should have the correct selected guid"
+ );
+ Assert.equal(
+ menuList.getAttribute("selectedGuid"),
+ expectedGuid,
+ "Menu list should have the correct selected guid"
+ );
+ Assert.equal(
+ menuList.label,
+ PlacesUtils.getString(expectedFolder),
+ "Should have kept the same menu label"
+ );
+
+ // Switch back to the toolbar folder.
+ const { toolbarGuid } = PlacesUtils.bookmarks;
+ promisePopup = BrowserTestUtils.waitForEvent(
+ menuList.menupopup,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(menuList, {});
+ await promisePopup;
+ EventUtils.synthesizeMouseAtCenter(
+ document.getElementById("editBMPanel_toolbarFolderItem"),
+ {}
+ );
+ await TestUtils.waitForCondition(
+ () => menuList.getAttribute("selectedGuid") == toolbarGuid,
+ "Should select the toolbar folder item"
+ );
+
+ // Save the bookmark.
+ const promiseBookmarkAdded =
+ PlacesTestUtils.waitForNotification("bookmark-added");
+ await hideBookmarksPanel();
+ const [{ parentGuid }] = await promiseBookmarkAdded;
+ Assert.equal(
+ parentGuid,
+ toolbarGuid,
+ "Should have saved the bookmark in the correct folder."
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js b/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
new file mode 100644
index 0000000000..23eec11e1d
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TEST_URL = "about:robots";
+StarUI._createPanelIfNeeded();
+const bookmarkPanel = document.getElementById("editBookmarkPanel");
+let folders;
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ bookmarkPanel.setAttribute("animate", false);
+
+ let oldTimeout = StarUI._autoCloseTimeout;
+ // Make the timeout something big, so it doesn't iteract badly with tests.
+ StarUI._autoCloseTimeout = 6000000;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ registerCleanupFunction(async () => {
+ StarUI._autoCloseTimeout = oldTimeout;
+ BrowserTestUtils.removeTab(tab);
+ bookmarkPanel.removeAttribute("animate");
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_newFolder() {
+ let newBookmarkObserver = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(({ url }) => url === TEST_URL)
+ );
+ await clickBookmarkStar();
+
+ // Open folder selector.
+ document.getElementById("editBMPanel_foldersExpander").click();
+
+ let folderTree = document.getElementById("editBMPanel_folderTree");
+
+ // Create new folder.
+ let newFolderButton = document.getElementById("editBMPanel_newFolderButton");
+ newFolderButton.click();
+
+ let newFolderGuid;
+ let newFolderObserver = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => {
+ for (let { guid, itemType } of events) {
+ newFolderGuid = guid;
+ if (itemType == PlacesUtils.bookmarks.TYPE_FOLDER) {
+ return true;
+ }
+ }
+ return false;
+ }
+ );
+
+ let menulist = document.getElementById("editBMPanel_folderMenuList");
+
+ await newFolderObserver;
+
+ // Wait for the folder to be created and for editing to start.
+ await TestUtils.waitForCondition(
+ () => folderTree.hasAttribute("editing"),
+ "Should be in edit mode for the new folder"
+ );
+
+ Assert.equal(
+ menulist.selectedItem.label,
+ newFolderButton.label,
+ "Should have the new folder selected by default"
+ );
+
+ let renameObserver = PlacesTestUtils.waitForNotification(
+ "bookmark-title-changed",
+ events => events.some(e => e.title === "f")
+ );
+
+ // Enter a new name.
+ EventUtils.synthesizeKey("f", {}, window);
+ EventUtils.synthesizeKey("VK_RETURN", {}, window);
+
+ await renameObserver;
+
+ await TestUtils.waitForCondition(
+ () => !folderTree.hasAttribute("editing"),
+ "Should have stopped editing the new folder"
+ );
+
+ Assert.equal(
+ menulist.selectedItem.label,
+ "f",
+ "Should have the new folder title"
+ );
+
+ await hideBookmarksPanel();
+ await newBookmarkObserver;
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url: TEST_URL });
+
+ Assert.equal(
+ bookmark.parentGuid,
+ newFolderGuid,
+ "The bookmark should be parented by the new folder"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js b/browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js
new file mode 100644
index 0000000000..67d1406bc1
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+
+add_task(async function test_change_title_from_BookmarkStar() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "Before Edit",
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ StarUI._createPanelIfNeeded();
+ let bookmarkPanel = document.getElementById("editBookmarkPanel");
+ let shownPromise = promisePopupShown(bookmarkPanel);
+
+ let bookmarkStar = BookmarkingUI.star;
+ bookmarkStar.click();
+
+ await shownPromise;
+
+ window.gEditItemOverlay.toggleFolderTreeVisibility();
+
+ let folderTree = document.getElementById("editBMPanel_folderTree");
+
+ // canDrop should always return false.
+ let bookmarkWithId = JSON.stringify(
+ Object.assign({
+ url: "http://example.com",
+ title: "Fake BM",
+ })
+ );
+
+ let dt = {
+ dropEffect: "move",
+ mozCursor: "auto",
+ mozItemCount: 1,
+ types: [PlacesUtils.TYPE_X_MOZ_PLACE],
+ mozTypesAt(i) {
+ return this.types;
+ },
+ mozGetDataAt(i) {
+ return bookmarkWithId;
+ },
+ };
+
+ Assert.ok(
+ !folderTree.view.canDrop(1, Ci.nsITreeView.DROP_BEFORE, dt),
+ "Should not be able to drop a bookmark"
+ );
+
+ // User Actions should be disabled.
+ const userActions = [
+ "cmd_undo",
+ "cmd_redo",
+ "cmd_cut",
+ "cmd_copy",
+ "cmd_paste",
+ "cmd_delete",
+ "cmd_selectAll",
+ // Anything starting with placesCmd_ should also be disabled.
+ "placesCmd_",
+ ];
+ for (let action of userActions) {
+ Assert.ok(
+ !folderTree.view._controller.supportsCommand(action),
+ `${action} should be disabled for the folder tree in bookmarks properties`
+ );
+ }
+
+ let hiddenPromise = promisePopupHidden(bookmarkPanel);
+ let doneButton = document.getElementById("editBookmarkPanelDoneButton");
+ doneButton.click();
+ await hiddenPromise;
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
new file mode 100644
index 0000000000..d98b7477ec
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js
@@ -0,0 +1,67 @@
+"use strict";
+
+add_task(async function test_dialog() {
+ info("Bug 479348 - Properties dialog on a root should be read-only.");
+ await withSidebarTree("bookmarks", async function (tree) {
+ tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
+ Assert.ok(
+ !tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "'placesCmd_show:info' on current selected node is disabled"
+ );
+
+ await withBookmarksDialog(
+ true,
+ function openDialog() {
+ // Even if the cmd is disabled, we can execute it regardless.
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ // Check that the dialog is read-only.
+ Assert.ok(dialogWin.gEditItemOverlay.readOnly, "Dialog is read-only");
+ // Check that accept button is disabled
+ let acceptButton = dialogWin.document
+ .getElementById("bookmarkpropertiesdialog")
+ .getButton("accept");
+ Assert.ok(acceptButton.disabled, "Accept button is disabled");
+
+ // Check that name picker is read only
+ let namepicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+ Assert.ok(namepicker.readOnly, "Name field is read-only");
+ Assert.equal(
+ namepicker.value,
+ PlacesUtils.getString("OtherBookmarksFolderTitle"),
+ "Node title is correct"
+ );
+ }
+ );
+ });
+});
+
+add_task(async function test_library() {
+ info("Bug 479348 - Library info pane on a root should be read-only.");
+ let library = await promiseLibrary("UnfiledBookmarks");
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ });
+ let PlacesOrganizer = library.PlacesOrganizer;
+ let tree = PlacesOrganizer._places;
+ tree.focus();
+ Assert.ok(
+ !tree.controller.isCommandEnabled("placesCmd_show:info"),
+ "'placesCmd_show:info' on current selected node is disabled"
+ );
+
+ // Check that the pane is read-only.
+ Assert.ok(library.gEditItemOverlay.readOnly, "Info pane is read-only");
+
+ // Check that name picker is read only
+ let namepicker = library.document.getElementById("editBMPanel_namePicker");
+ Assert.ok(namepicker.readOnly, "Name field is read-only");
+ Assert.equal(
+ namepicker.value,
+ PlacesUtils.getString("OtherBookmarksFolderTitle"),
+ "Node title is correct"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js b/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
new file mode 100644
index 0000000000..99da75d62f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * Tests that multiple tags can be added to a bookmark using the star-shaped button, the library and the sidebar.
+ */
+
+StarUI._createPanelIfNeeded();
+const bookmarkPanel = document.getElementById("editBookmarkPanel");
+let folders;
+
+async function openPopupAndSelectFolder(guid, newBookmark = false) {
+ await clickBookmarkStar();
+
+ let notificationPromise;
+ if (!newBookmark) {
+ notificationPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-moved",
+ events => events.some(e => guid === e.parentGuid)
+ );
+ }
+
+ // Expand the folder tree.
+ document.getElementById("editBMPanel_foldersExpander").click();
+ document.getElementById("editBMPanel_folderTree").selectItems([guid]);
+
+ await hideBookmarksPanel();
+ if (!newBookmark) {
+ await notificationPromise;
+ }
+}
+
+async function assertRecentFolders(expectedGuids, msg) {
+ await clickBookmarkStar();
+
+ let actualGuids = [];
+ function getGuids() {
+ actualGuids = [];
+ const folderMenuPopup = document.getElementById(
+ "editBMPanel_folderMenuList"
+ ).menupopup;
+
+ let separatorFound = false;
+ // The list of folders goes from editBMPanel_foldersSeparator to the end.
+ for (let child of folderMenuPopup.children) {
+ if (separatorFound) {
+ actualGuids.push(child.folderGuid);
+ } else if (child.id == "editBMPanel_foldersSeparator") {
+ separatorFound = true;
+ }
+ }
+ }
+
+ // The dialog fills in the folder list asnychronously, so we might need to wait
+ // for that to complete.
+ await TestUtils.waitForCondition(() => {
+ getGuids();
+ return actualGuids.length == expectedGuids.length;
+ }, `Should have opened dialog with expected recent folders for: ${msg}`);
+
+ Assert.deepEqual(actualGuids, expectedGuids, msg);
+
+ await hideBookmarksPanel();
+
+ // Give the metadata chance to be written to the database before we attempt
+ // to open the dialog again.
+ let diskGuids = [];
+ await TestUtils.waitForCondition(async () => {
+ diskGuids = await PlacesUtils.metadata.get(
+ PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
+ []
+ );
+ return diskGuids.length == expectedGuids.length;
+ }, `Should have written data to disk for: ${msg}`);
+
+ Assert.deepEqual(
+ diskGuids,
+ expectedGuids,
+ `Should match the disk GUIDS for ${msg}`
+ );
+}
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.metadata.delete(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY);
+
+ bookmarkPanel.setAttribute("animate", false);
+
+ let oldTimeout = StarUI._autoCloseTimeout;
+ // Make the timeout something big, so it doesn't iteract badly with tests.
+ StarUI._autoCloseTimeout = 6000000;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:robots",
+ waitForStateStop: true,
+ });
+
+ folders = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "Bob",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "Place",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "Delight",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "Surprise",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "Treble Bob",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "Principal",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "Max Default Recent Folders",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "One Over Default Maximum Recent Folders",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ StarUI._autoCloseTimeout = oldTimeout;
+ BrowserTestUtils.removeTab(tab);
+ bookmarkPanel.removeAttribute("animate");
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.metadata.delete(PlacesUIUtils.LAST_USED_FOLDERS_META_KEY);
+ });
+});
+
+add_task(async function test_remember_last_folder() {
+ await assertRecentFolders([], "Should have no recent folders to start with.");
+
+ await openPopupAndSelectFolder(folders[0].guid, true);
+
+ await assertRecentFolders(
+ [folders[0].guid],
+ "Should have one folder in the list."
+ );
+});
+
+add_task(async function test_forget_oldest_folder() {
+ // Add some more folders.
+ let expectedFolders = [folders[0].guid];
+ for (let i = 1; i < folders.length; i++) {
+ await assertRecentFolders(
+ expectedFolders,
+ "Should have only the expected folders in the list"
+ );
+
+ await openPopupAndSelectFolder(folders[i].guid);
+
+ expectedFolders.unshift(folders[i].guid);
+ if (expectedFolders.length > PlacesUIUtils.maxRecentFolders) {
+ expectedFolders.pop();
+ }
+ }
+
+ await assertRecentFolders(
+ expectedFolders,
+ "Should have expired the original folder"
+ );
+});
+
+add_task(async function test_reorder_folders() {
+ let expectedFolders = [
+ folders[2].guid,
+ folders[7].guid,
+ folders[6].guid,
+ folders[5].guid,
+ folders[4].guid,
+ folders[3].guid,
+ folders[1].guid,
+ ];
+
+ // Take an old one and put it at the front.
+ await openPopupAndSelectFolder(folders[2].guid);
+
+ await assertRecentFolders(
+ expectedFolders,
+ "Should have correctly re-ordered the list"
+ );
+});
+
+add_task(async function test_change_max_recent_folders_pref() {
+ let expectedFolders = [folders[0].guid];
+
+ Services.prefs.setIntPref("browser.bookmarks.editDialog.maxRecentFolders", 1);
+
+ await openPopupAndSelectFolder(folders[1].guid);
+ await openPopupAndSelectFolder(folders[0].guid);
+
+ await assertRecentFolders(
+ expectedFolders,
+ "Should have only one recent folder in the bookmark edit panel"
+ );
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref(
+ "browser.bookmarks.editDialog.maxRecentFolders"
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js b/browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js
new file mode 100644
index 0000000000..621ea19bb9
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Test to ensure that on "mousedown" in Toolbar we set Speculative Connection
+ */
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const sandbox = sinon.createSandbox();
+let spy = sandbox
+ .stub(PlacesUIUtils, "setupSpeculativeConnection")
+ .returns(Promise.resolve());
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let toolbar = document.getElementById("PersonalToolbar");
+ ok(toolbar, "PersonalToolbar should not be null");
+
+ if (toolbar.collapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(function () {
+ return promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ sandbox.restore();
+ });
+});
+
+add_task(async function checkToolbarSpeculativeConnection() {
+ let toolbarBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "https://example.com/",
+ title: "Bookmark 1",
+ });
+ let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid);
+
+ info("Synthesize mousedown on selected bookmark");
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ type: "mousedown",
+ });
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ type: "mouseup",
+ });
+
+ Assert.ok(spy.called, "Speculative connection for Toolbar called");
+ sandbox.restore();
+});
+
+add_task(async function checkMenuSpeculativeConnection() {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ info("Placing a Menu widget");
+ let origBMBlocation = CustomizableUI.getPlacementOfWidget(
+ "bookmarks-menu-button"
+ );
+ // Ensure BMB is available in UI.
+ if (!origBMBlocation) {
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ }
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ // if BMB was not originally in UI, remove it.
+ if (!origBMBlocation) {
+ CustomizableUI.removeWidgetFromArea("bookmarks-menu-button");
+ }
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://example.com/",
+ title: "Bookmark 2",
+ });
+
+ // Test Bookmarks Menu Button
+ let BMB = document.getElementById("bookmarks-menu-button");
+ let BMBpopup = document.getElementById("BMB_bookmarksPopup");
+ let promiseEvent = BrowserTestUtils.waitForEvent(BMBpopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(BMB, {});
+ await promiseEvent;
+ info("Popupshown on Bookmarks-Menu-Button");
+
+ let menuBookmark = [...BMBpopup.children].find(
+ node => node.label == "Bookmark 2"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(menuBookmark, {
+ type: "mousedown",
+ });
+ EventUtils.synthesizeMouseAtCenter(menuBookmark, {
+ type: "mouseup",
+ });
+
+ Assert.ok(spy.called, "Speculative connection for Menu Button called");
+ sandbox.restore();
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js b/browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js
new file mode 100644
index 0000000000..1ba6f56949
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js
@@ -0,0 +1,47 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+add_task(async function () {
+ let mainFolder = await PlacesUtils.bookmarks.insert({
+ title: "mainFolder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ await PlacesUtils.bookmarks.insertTree({
+ guid: mainFolder.guid,
+ children: [
+ {
+ title: "firstFolder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ {
+ title: "secondFolder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ ],
+ });
+ tree.selectItems([mainFolder.guid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ let firstFolder = tree.selectedNode.getChild(0);
+
+ tree.selectNode(firstFolder);
+ info("Synthesize click on selected node to open it.");
+ synthesizeClickOnSelectedTreeCell(tree);
+ info(`Get the hashed uri starts with "place:" and hash key&value pairs.`);
+ let hashedKey = PlacesUIUtils.obfuscateUrlForXulStore(firstFolder.uri);
+
+ let docUrl = "chrome://browser/content/places/bookmarksSidebar.xhtml";
+
+ let value = Services.xulStore.getValue(docUrl, hashedKey, "open");
+
+ Assert.ok(hashedKey.startsWith("place:"), "Sanity check the hashed key");
+ Assert.equal(value, "true", "Check the expected xulstore value");
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_add_tags.js b/browser/components/places/tests/browser/browser_bookmark_add_tags.js
new file mode 100644
index 0000000000..b031e1d219
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_add_tags.js
@@ -0,0 +1,228 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Tests that multiple tags can be added to a bookmark using the star-shaped button, the library and the sidebar.
+ */
+let bookmarkPanel;
+let bookmarkStar;
+
+async function clickBookmarkStar() {
+ let shownPromise = promisePopupShown(bookmarkPanel);
+ bookmarkStar.click();
+ await shownPromise;
+}
+
+async function hideBookmarksPanel(callback) {
+ let hiddenPromise = promisePopupHidden(bookmarkPanel);
+ callback();
+ await hiddenPromise;
+}
+
+registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_add_bookmark_tags_from_bookmarkProperties() {
+ const TEST_URL = "about:robots";
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: "about:buildconfig",
+ title: "Bookmark Title",
+ });
+
+ PlacesUtils.tagging.tagURI(makeURI("about:buildconfig"), ["tag0"]);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ win.StarUI._createPanelIfNeeded();
+ win.StarUI._autoCloseTimeout = 1000;
+ bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+ bookmarkPanel.setAttribute("animate", false);
+ bookmarkStar = win.BookmarkingUI.star;
+
+ // Cleanup.
+ registerCleanupFunction(async function () {
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.closeWindow(win);
+ });
+
+ let bookmarkPanelTitle = win.document.getElementById(
+ "editBookmarkPanelTitle"
+ );
+
+ // The bookmarks panel is expected to auto-close after this step.
+ let promiseNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(({ url }) => !url || url == TEST_URL)
+ );
+ await hideBookmarksPanel(async () => {
+ // Click the bookmark star to bookmark the page.
+ await clickBookmarkStar();
+ await TestUtils.waitForCondition(
+ () =>
+ win.document.l10n.getAttributes(bookmarkPanelTitle).id ===
+ "bookmarks-add-bookmark",
+ "Bookmark title is correct"
+ );
+ Assert.equal(
+ bookmarkStar.getAttribute("starred"),
+ "true",
+ "Page is starred"
+ );
+ });
+ await promiseNotification;
+
+ // Click the bookmark star again to add tags.
+ await clickBookmarkStar();
+ promiseNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-tags-changed"
+ );
+ await TestUtils.waitForCondition(
+ () =>
+ win.document.l10n.getAttributes(bookmarkPanelTitle).id ===
+ "bookmarks-edit-bookmark",
+ "Bookmark title is correct"
+ );
+ await fillBookmarkTextField("editBMPanel_tagsField", "tag1", win);
+ const doneButton = win.document.getElementById("editBookmarkPanelDoneButton");
+ await hideBookmarksPanel(() => doneButton.click());
+ await promiseNotification;
+ Assert.equal(
+ PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)).length,
+ 1,
+ "Found the right number of tags"
+ );
+ Assert.deepEqual(
+ PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)),
+ ["tag1"]
+ );
+
+ // Click the bookmark star again, add more tags.
+ await clickBookmarkStar();
+ promiseNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-tags-changed"
+ );
+ await fillBookmarkTextField("editBMPanel_tagsField", "tag1, tag2, tag3", win);
+ await hideBookmarksPanel(() => doneButton.click());
+ await promiseNotification;
+
+ const bookmarks = [];
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm =>
+ bookmarks.push(bm)
+ );
+ Assert.equal(bookmarks.length, 1, "Only one bookmark should exist");
+ Assert.equal(
+ PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)).length,
+ 3,
+ "Found the right number of tags"
+ );
+ Assert.deepEqual(
+ PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL)),
+ ["tag1", "tag2", "tag3"]
+ );
+
+ // Cleanup.
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_add_bookmark_tags_from_library() {
+ const uri = "http://example.com/";
+
+ // Add a bookmark.
+ await PlacesUtils.bookmarks.insert({
+ url: uri,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ // Open the Library on "UnfiledBookmarks".
+ let library = await promiseLibrary("UnfiledBookmarks");
+
+ // Cleanup.
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ });
+
+ let bookmarkNode = library.ContentTree.view.selectedNode;
+ Assert.equal(
+ bookmarkNode.uri,
+ "http://example.com/",
+ "Found the expected bookmark"
+ );
+
+ // Add a tag to the bookmark.
+ fillBookmarkTextField("editBMPanel_tagsField", "tag1", library);
+
+ await TestUtils.waitForCondition(
+ () => bookmarkNode.tags === "tag1",
+ "Node tag is correct"
+ );
+
+ // Add a new tag to the bookmark.
+ fillBookmarkTextField("editBMPanel_tagsField", "tag1, tag2", library);
+
+ await TestUtils.waitForCondition(
+ () => bookmarkNode.tags === "tag1,tag2",
+ "Node tag is correct"
+ );
+
+ // Check the tag change has been completed.
+ let tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(uri));
+ Assert.equal(tags.length, 2, "Found the right number of tags");
+ Assert.deepEqual(tags, ["tag1", "tag2"], "Found the expected tags");
+
+ await promiseLibraryClosed(library);
+ // Cleanup.
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_add_bookmark_tags_from_sidebar() {
+ const TEST_URL = "about:buildconfig";
+
+ let bookmarks = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "Bookmark Title",
+ });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ tree.selectItems([bookmarks.guid]);
+ // Add one tag.
+ await addTags(["tag1"], tree, ["tag1"]);
+ // Add 2 more tags.
+ await addTags(["tag2", "tag3"], tree, ["tag1", "tag2", "tag3"]);
+ });
+
+ async function addTags(tagValue, tree, expected) {
+ await withBookmarksDialog(
+ false,
+ function openPropertiesDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ PlacesUtils.tagging.tagURI(makeURI(TEST_URL), tagValue);
+ let tags = PlacesUtils.tagging.getTagsForURI(
+ Services.io.newURI(TEST_URL)
+ );
+
+ Assert.deepEqual(tags, expected, "Tags field is correctly populated");
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ }
+ );
+ }
+
+ // Cleanup.
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_all_tabs.js b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js
new file mode 100644
index 0000000000..2852bf4019
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_all_tabs.js
@@ -0,0 +1,46 @@
+/**
+ * Test for Bug 446171 - Name field of bookmarks saved via 'Bookmark All Tabs'
+ * has '(null)' value if history is disabled or just in private browsing mode
+ */
+"use strict";
+
+add_task(async function () {
+ const BASE_URL =
+ "http://example.org/browser/browser/components/places/tests/browser/";
+ const TEST_PAGES = [
+ BASE_URL + "bookmark_dummy_1.html",
+ BASE_URL + "bookmark_dummy_2.html",
+ BASE_URL + "bookmark_dummy_1.html",
+ ];
+
+ function promiseAddTab(url) {
+ return BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ }
+
+ let tabs = await Promise.all(TEST_PAGES.map(promiseAddTab));
+
+ let URIs = PlacesCommandHook.uniqueCurrentPages;
+ is(URIs.length, 3, "Only unique pages are returned");
+
+ Assert.deepEqual(
+ URIs.map(URI => URI.uri.spec),
+ [
+ "about:blank",
+ BASE_URL + "bookmark_dummy_1.html",
+ BASE_URL + "bookmark_dummy_2.html",
+ ],
+ "Correct URIs are returned"
+ );
+
+ Assert.deepEqual(
+ URIs.map(URI => URI.title),
+ ["New Tab", "Bookmark Dummy 1", "Bookmark Dummy 2"],
+ "Correct titles are returned"
+ );
+
+ registerCleanupFunction(async function () {
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js b/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js
new file mode 100644
index 0000000000..8b954a8469
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_backup_export_import.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests bookmarks backup export/import as JSON file.
+ */
+
+const BASE_URL = "http://example.com/";
+
+const PLACES = [
+ {
+ guid: PlacesUtils.bookmarks.menuGuid,
+ prefix: "In Menu",
+ total: 5,
+ },
+ {
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ prefix: "In Toolbar",
+ total: 7,
+ },
+ {
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ prefix: "In Other",
+ total: 8,
+ },
+];
+
+var importExportPicker, saveDir, actualBookmarks;
+
+async function generateTestBookmarks() {
+ actualBookmarks = [];
+ for (let place of PLACES) {
+ let currentPlaceChildren = [];
+ for (let i = 1; i <= place.total; i++) {
+ currentPlaceChildren.push({
+ url: `${BASE_URL}${i}`,
+ title: `${place.prefix} Bookmark: ${i}`,
+ });
+ }
+ await PlacesUtils.bookmarks.insertTree({
+ guid: place.guid,
+ children: currentPlaceChildren,
+ });
+ actualBookmarks = actualBookmarks.concat(currentPlaceChildren);
+ }
+}
+
+async function validateImportedBookmarksByParent(
+ parentGuid,
+ expectedChildrenTotal
+) {
+ let currentPlace = PLACES.filter(elem => {
+ return elem.guid === parentGuid.toString();
+ })[0];
+
+ let bookmarksTree = await PlacesUtils.promiseBookmarksTree(parentGuid);
+
+ Assert.equal(
+ bookmarksTree.children.length,
+ expectedChildrenTotal,
+ `Imported bookmarks length should be ${expectedChildrenTotal}`
+ );
+
+ for (let importedBookmark of bookmarksTree.children) {
+ Assert.equal(
+ importedBookmark.type,
+ PlacesUtils.TYPE_X_MOZ_PLACE,
+ `Exported bookmarks should be of type bookmark`
+ );
+
+ let doesTitleContain = importedBookmark.title
+ .toString()
+ .includes(`${currentPlace.prefix} Bookmark`);
+ Assert.equal(
+ doesTitleContain,
+ true,
+ `Bookmark title should contain text: ${currentPlace.prefix} Bookmark`
+ );
+
+ let doesUriContains = importedBookmark.uri.toString().includes(BASE_URL);
+ Assert.equal(doesUriContains, true, "Bookmark uri should contain base url");
+ }
+}
+
+async function validateImportedBookmarks(fromPlaces) {
+ for (let i = 0; i < fromPlaces.length; i++) {
+ let parentContainer = fromPlaces[i];
+ await validateImportedBookmarksByParent(
+ parentContainer.guid,
+ parentContainer.total
+ );
+ }
+}
+
+async function promiseImportExport(aWindow) {
+ saveDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ saveDir.append("temp-bookmarks-export");
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ importExportPicker.displayDirectory = saveDir;
+
+ return new Promise(resolve => {
+ importExportPicker.showCallback = async () => {
+ let fileName = "bookmarks-backup.json";
+ let destFile = saveDir.clone();
+ destFile.append(fileName);
+ importExportPicker.setFiles([destFile]);
+ resolve(destFile);
+ };
+ });
+}
+
+add_setup(async function () {
+ await promisePlacesInitComplete();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await generateTestBookmarks();
+ importExportPicker = SpecialPowers.MockFilePicker;
+ importExportPicker.init(window);
+
+ registerCleanupFunction(async () => {
+ importExportPicker.cleanup();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+async function showMaintenancePopup(libraryWindow) {
+ let button = libraryWindow.document.getElementById("maintenanceButton");
+ let popup = libraryWindow.document.getElementById("maintenanceButtonPopup");
+ let shown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+
+ info("Clicking maintenance menu");
+
+ button.openMenu(true);
+
+ await shown;
+ info("Maintenance popup shown");
+ return popup;
+}
+
+add_task(async function test_export_json() {
+ let libraryWindow = await promiseLibrary();
+ let popup = await showMaintenancePopup(libraryWindow);
+ let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+
+ info("Activating #backupBookmarks");
+
+ let backupPromise = promiseImportExport();
+
+ popup.activateItem(popup.querySelector("#backupBookmarks"));
+ await hidden;
+
+ info("Popup hidden");
+
+ let backupFile = await backupPromise;
+ await TestUtils.waitForCondition(
+ backupFile.exists,
+ "Backup file should exist"
+ );
+ await promiseLibraryClosed(libraryWindow);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+async function showFileRestorePopup(libraryWindow) {
+ let parentPopup = await showMaintenancePopup(libraryWindow);
+ let popup = parentPopup.querySelector("#fileRestorePopup");
+ let shown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ parentPopup.querySelector("#fileRestoreMenu").openMenu(true);
+ await shown;
+ return popup;
+}
+
+add_task(async function test_import_json() {
+ let libraryWindow = await promiseLibrary();
+ let popup = await showFileRestorePopup(libraryWindow);
+
+ let backupPromise = promiseImportExport();
+ let dialogPromise = BrowserTestUtils.promiseAlertDialogOpen("accept");
+
+ let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden");
+ popup.activateItem(popup.querySelector("#restoreFromFile"));
+ await hidden;
+
+ await backupPromise;
+ await dialogPromise;
+
+ let restored = 0;
+ let promiseBookmarksRestored = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(() => ++restored == actualBookmarks.length)
+ );
+
+ await promiseBookmarksRestored;
+ await validateImportedBookmarks(PLACES);
+ await promiseLibraryClosed(libraryWindow);
+
+ registerCleanupFunction(async () => {
+ if (saveDir) {
+ saveDir.remove(true);
+ }
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_change_location.js b/browser/components/places/tests/browser/browser_bookmark_change_location.js
new file mode 100644
index 0000000000..3a82b67a93
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_change_location.js
@@ -0,0 +1,213 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that the bookmark location (url) can be changed from the toolbar and the sidebar.
+ */
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+const TEST_URL2 = "about:credits";
+const TEST_URL3 = "about:config";
+
+// Setup.
+add_setup(async function () {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "always"]],
+ });
+
+ // The following initialization code is necessary to avoid a frequent
+ // intermittent failure in verify-fission where, due to timings, we may or
+ // may not import default bookmarks. We also want to avoid the empty toolbar
+ // placeholder shifting stuff around.
+ info("Ensure Places init is complete");
+ let placesInitCompleteObserved = TestUtils.topicObserved(
+ "places-browser-init-complete"
+ );
+ Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "browser-glue-test", "places-browser-init-complete");
+ await placesInitCompleteObserved;
+ info("Add a bookmark to avoid showing the empty toolbar placeholder.");
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "initial",
+ url: TEST_URL,
+ });
+
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+ if (wasCollapsed) {
+ info("Show the bookmarks toolbar");
+ await promiseSetToolbarVisibility(toolbar, true);
+ info("Ensure toolbar visibility was updated");
+ await BrowserTestUtils.waitForEvent(
+ toolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ }
+
+ // Cleanup.
+ registerCleanupFunction(async () => {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_change_location_from_Toolbar() {
+ let toolbarBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "",
+ url: TEST_URL,
+ });
+
+ let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid);
+
+ await withBookmarksDialog(
+ false,
+ async function openPropertiesDialog() {
+ let placesContext = document.getElementById("placesContext");
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await promisePopup;
+
+ let properties = document.getElementById(
+ "placesContext_show_bookmark:info"
+ );
+ placesContext.activateItem(properties);
+ },
+ async function test(dialogWin) {
+ // Check the initial location.
+ let locationPicker = dialogWin.document.getElementById(
+ "editBMPanel_locationField"
+ );
+ Assert.equal(
+ locationPicker.value,
+ TEST_URL,
+ "EditBookmark: The current location is the expected one."
+ );
+
+ // To check whether the lastModified field will be updated correctly.
+ let lastModified = _getLastModified(toolbarBookmark.guid);
+
+ // Update the "location" field.
+ fillBookmarkTextField(
+ "editBMPanel_locationField",
+ TEST_URL2,
+ dialogWin,
+ false
+ );
+
+ locationPicker.blur();
+
+ Assert.equal(
+ locationPicker.value,
+ TEST_URL2,
+ "EditBookmark: The changed location is the expected one."
+ );
+
+ locationPicker.focus();
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+
+ await TestUtils.waitForCondition(
+ () => _getLastModified(toolbarBookmark.guid) > lastModified,
+ "EditBookmark: The lastModified will be greater than before updating."
+ );
+ }
+ );
+
+ let updatedBm = await PlacesUtils.bookmarks.fetch(toolbarBookmark.guid);
+ Assert.equal(
+ updatedBm.url,
+ TEST_URL2,
+ "EditBookmark: Should have updated the bookmark location in the database."
+ );
+});
+
+add_task(async function test_change_location_from_Sidebar() {
+ let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL2 });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ tree.selectItems([bm.guid]);
+
+ await withBookmarksDialog(
+ false,
+ function openPropertiesDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ // Check the initial location.
+ let locationPicker = dialogWin.document.getElementById(
+ "editBMPanel_locationField"
+ );
+ Assert.equal(
+ locationPicker.value,
+ TEST_URL2,
+ "Sidebar - EditBookmark: The current location is the expected one."
+ );
+
+ // To check whether the lastModified field will be updated correctly.
+ let lastModified = _getLastModified(bm.guid);
+
+ // Update the "location" field.
+ fillBookmarkTextField(
+ "editBMPanel_locationField",
+ TEST_URL3,
+ dialogWin,
+ false
+ );
+
+ Assert.equal(
+ locationPicker.value,
+ TEST_URL3,
+ "Sidebar - EditBookmark: The location is changed in dialog for prefered one."
+ );
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+
+ await TestUtils.waitForCondition(
+ () => _getLastModified(bm.guid) > lastModified,
+ "Sidebar - EditBookmark: The lastModified will be greater than before updating."
+ );
+ }
+ );
+
+ let updatedBm = await PlacesUtils.bookmarks.fetch(bm.guid);
+ Assert.equal(
+ updatedBm.url,
+ TEST_URL3,
+ "Sidebar - EditBookmark: Should have updated the bookmark location in the database."
+ );
+ });
+});
+
+function _getLastModified(guid) {
+ const toolbarNode = PlacesUtils.getFolderContents(
+ PlacesUtils.bookmarks.toolbarGuid
+ ).root;
+
+ try {
+ for (let i = 0; i < toolbarNode.childCount; i++) {
+ const node = toolbarNode.getChild(i);
+ if (node.bookmarkGuid === guid) {
+ return node.lastModified;
+ }
+ }
+
+ throw new Error(`Node for ${guid} was not found`);
+ } finally {
+ toolbarNode.containerOpen = false;
+ }
+}
diff --git a/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js
new file mode 100644
index 0000000000..ac9120d3d6
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js
@@ -0,0 +1,798 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test removing bookmarks from the Bookmarks Toolbar and Library.
+ */
+const SECOND_BOOKMARK_TITLE = "Second Bookmark Title";
+const bookmarksInfo = [
+ {
+ title: "firefox",
+ url: "http://example.com",
+ },
+ {
+ title: "rules",
+ url: "http://example.com/2",
+ },
+ {
+ title: "yo",
+ url: "http://example.com/2",
+ },
+];
+const TEST_URL = "about:mozilla";
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "userContextEnabled",
+ "privacy.userContext.enabled"
+);
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ registerCleanupFunction(async () => {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+let OptionItemExists = (elementId, doc = document) => {
+ let optionItem = doc.getElementById(elementId);
+
+ Assert.ok(optionItem, `Context menu contains the menuitem ${elementId}`);
+ Assert.ok(
+ BrowserTestUtils.isVisible(optionItem),
+ `Context menu option ${elementId} is visible`
+ );
+};
+
+let OptionsMatchExpected = (contextMenu, expectedOptionItems) => {
+ let idList = [];
+ for (let elem of contextMenu.children) {
+ if (
+ BrowserTestUtils.isVisible(elem) &&
+ elem.localName !== "menuseparator"
+ ) {
+ idList.push(elem.id);
+ }
+ }
+
+ Assert.deepEqual(
+ idList.sort(),
+ expectedOptionItems.sort(),
+ "Content is the same across both lists"
+ );
+};
+
+let checkContextMenu = async (cbfunc, optionItems, doc = document) => {
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: SECOND_BOOKMARK_TITLE,
+ url: TEST_URL,
+ });
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: bookmarksInfo,
+ });
+
+ // Open and check the context menu twice, once with
+ // `browser.tabs.loadBookmarksInTabs` set to true and again with it set to
+ // false.
+ for (let loadBookmarksInNewTab of [true, false]) {
+ info(
+ `Running checkContextMenu: ` + JSON.stringify({ loadBookmarksInNewTab })
+ );
+
+ Services.prefs.setBoolPref(
+ "browser.tabs.loadBookmarksInTabs",
+ loadBookmarksInNewTab
+ );
+
+ // When `loadBookmarksInTabs` is true, the usual placesContext_open:newtab
+ // item is hidden and placesContext_open is shown. The tasks in this test
+ // assume that `loadBookmarksInTabs` is false, so when a caller expects
+ // placesContext_open:newtab to appear but not placesContext_open, add it to
+ // the list of expected items when the pref is set.
+ let expectedOptionItems = [...optionItems];
+ if (
+ loadBookmarksInNewTab &&
+ optionItems.includes("placesContext_open:newtab") &&
+ !optionItems.includes("placesContext_open")
+ ) {
+ expectedOptionItems.push("placesContext_open");
+ }
+
+ // The caller is responsible for opening the menu, via `cbfunc()`.
+ let contextMenu = await cbfunc(bookmark);
+
+ for (let item of expectedOptionItems) {
+ OptionItemExists(item, doc);
+ }
+
+ OptionsMatchExpected(contextMenu, expectedOptionItems);
+
+ // Check the "default" attributes on placesContext_open and
+ // placesContext_open:newtab.
+ if (expectedOptionItems.includes("placesContext_open")) {
+ Assert.equal(
+ doc.getElementById("placesContext_open").getAttribute("default"),
+ loadBookmarksInNewTab ? "" : "true",
+ `placesContext_open has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}`
+ );
+ }
+ if (expectedOptionItems.includes("placesContext_open:newtab")) {
+ Assert.equal(
+ doc.getElementById("placesContext_open:newtab").getAttribute("default"),
+ loadBookmarksInNewTab ? "true" : "",
+ `placesContext_open:newtab has the correct "default" attribute when loadBookmarksInTabs = ${loadBookmarksInNewTab}`
+ );
+ }
+
+ contextMenu.hidePopup();
+ }
+
+ Services.prefs.clearUserPref("browser.tabs.loadBookmarksInTabs");
+ await PlacesUtils.bookmarks.eraseEverything();
+};
+
+add_task(async function test_bookmark_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_open:newtab",
+ "placesContext_open:newcontainertab",
+ "placesContext_open:newwindow",
+ "placesContext_open:newprivatewindow",
+ "placesContext_show_bookmark:info",
+ "placesContext_deleteBookmark",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ "placesContext_showAllBookmarks",
+ "toggle_PersonalToolbar",
+ "show-other-bookmarks_PersonalToolbar",
+ ];
+ if (!userContextEnabled) {
+ optionItems.splice(
+ optionItems.indexOf("placesContext_open:newcontainertab"),
+ 1
+ );
+ }
+
+ await checkContextMenu(async function () {
+ let toolbarBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "Bookmark Title",
+ url: TEST_URL,
+ });
+
+ let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid);
+
+ let contextMenu = document.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await popupShownPromise;
+ return contextMenu;
+ }, optionItems);
+
+ let tabs = [];
+ let contextMenuOnContent;
+
+ await checkContextMenu(async function () {
+ info("Check context menu after opening context menu on content");
+ const toolbarBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "Bookmark Title",
+ url: TEST_URL,
+ });
+
+ info("Open context menu on about:config");
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:config"
+ );
+ tabs.push(tab);
+ contextMenuOnContent = document.getElementById("contentAreaContextMenu");
+ const popupShownPromiseOnContent = BrowserTestUtils.waitForEvent(
+ contextMenuOnContent,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(tab.linkedBrowser, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await popupShownPromiseOnContent;
+ contextMenuOnContent.hidePopup();
+
+ info("Check context menu on bookmark");
+ const toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid);
+ const contextMenu = document.getElementById("placesContext");
+ const popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await popupShownPromise;
+
+ return contextMenu;
+ }, optionItems);
+
+ // We need to do a thorough cleanup to avoid leaking the window of
+ // 'about:config'.
+ for (let tab of tabs) {
+ const tabClosed = BrowserTestUtils.waitForTabClosing(tab);
+ BrowserTestUtils.removeTab(tab);
+ await tabClosed;
+ }
+});
+
+add_task(async function test_empty_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_openBookmarkContainer:tabs",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ "placesContext_paste",
+ "placesContext_showAllBookmarks",
+ "toggle_PersonalToolbar",
+ "show-other-bookmarks_PersonalToolbar",
+ ];
+
+ await checkContextMenu(async function () {
+ let contextMenu = document.getElementById("placesContext");
+ let toolbar = document.querySelector("#PlacesToolbarItems");
+ let openToolbarContextMenuPromise = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "shown"
+ );
+
+ // Use the end of the toolbar because the beginning (and even middle, on
+ // some resolutions) might be occluded by the empty toolbar message, which
+ // has a different context menu.
+ let bounds = toolbar.getBoundingClientRect();
+ EventUtils.synthesizeMouse(toolbar, bounds.width - 5, 5, {
+ type: "contextmenu",
+ });
+
+ await openToolbarContextMenuPromise;
+ return contextMenu;
+ }, optionItems);
+});
+
+add_task(async function test_separator_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_delete",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ "placesContext_showAllBookmarks",
+ "toggle_PersonalToolbar",
+ "show-other-bookmarks_PersonalToolbar",
+ ];
+
+ await checkContextMenu(async function () {
+ let sep = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+
+ let toolbarNode = getToolbarNodeForItemGuid(sep.guid);
+ let contextMenu = document.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await popupShownPromise;
+ return contextMenu;
+ }, optionItems);
+});
+
+add_task(async function test_folder_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_openBookmarkContainer:tabs",
+ "placesContext_show_folder:info",
+ "placesContext_deleteFolder",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ "placesContext_sortBy:name",
+ "placesContext_showAllBookmarks",
+ "toggle_PersonalToolbar",
+ "show-other-bookmarks_PersonalToolbar",
+ ];
+
+ await checkContextMenu(async function () {
+ let folder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+
+ let toolbarNode = getToolbarNodeForItemGuid(folder.guid);
+ let contextMenu = document.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await popupShownPromise;
+ return contextMenu;
+ }, optionItems);
+});
+
+add_task(async function test_sidebar_folder_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_show_folder:info",
+ "placesContext_deleteFolder",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_openBookmarkContainer:tabs",
+ "placesContext_sortBy:name",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ ];
+
+ await withSidebarTree("bookmarks", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ tree.selectItems([folder.guid]);
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_sidebar_multiple_folders_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_show_folder:info",
+ "placesContext_deleteFolder",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_sortBy:name",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ ];
+
+ await withSidebarTree("bookmarks", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ let folder1 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "folder 1",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ let folder2 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "folder 2",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ tree.selectItems([folder1.guid, folder2.guid]);
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_sidebar_bookmark_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_open:newtab",
+ "placesContext_open:newcontainertab",
+ "placesContext_open:newwindow",
+ "placesContext_open:newprivatewindow",
+ "placesContext_show_bookmark:info",
+ "placesContext_deleteBookmark",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ ];
+ if (!userContextEnabled) {
+ optionItems.splice(
+ optionItems.indexOf("placesContext_open:newcontainertab"),
+ 1
+ );
+ }
+
+ await withSidebarTree("bookmarks", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ tree.selectItems([bookmark.guid]);
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_sidebar_bookmark_search_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_open:newtab",
+ "placesContext_open:newcontainertab",
+ "placesContext_open:newwindow",
+ "placesContext_open:newprivatewindow",
+ "placesContext_showInFolder",
+ "placesContext_show_bookmark:info",
+ "placesContext_deleteBookmark",
+ "placesContext_cut",
+ "placesContext_copy",
+ ];
+ if (!userContextEnabled) {
+ optionItems.splice(
+ optionItems.indexOf("placesContext_open:newcontainertab"),
+ 1
+ );
+ }
+
+ await withSidebarTree("bookmarks", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ info("Checking bookmark sidebar menu contents in search context");
+ // Perform a search first
+ let searchBox =
+ SidebarUI.browser.contentDocument.getElementById("search-box");
+ searchBox.value = SECOND_BOOKMARK_TITLE;
+ searchBox.doCommand();
+ tree.selectItems([bookmark.guid]);
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_library_bookmark_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_open",
+ "placesContext_open:newtab",
+ "placesContext_open:newcontainertab",
+ "placesContext_open:newwindow",
+ "placesContext_open:newprivatewindow",
+ "placesContext_deleteBookmark",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ ];
+ if (!userContextEnabled) {
+ optionItems.splice(
+ optionItems.indexOf("placesContext_open:newcontainertab"),
+ 1
+ );
+ }
+
+ await withLibraryWindow("BookmarksToolbar", async ({ left, right }) => {
+ await checkContextMenu(
+ async bookmark => {
+ let contextMenu = right.ownerDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ right.selectItems([bookmark.guid]);
+ synthesizeClickOnSelectedTreeCell(right, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ right.ownerDocument
+ );
+ });
+});
+
+add_task(async function test_library_bookmark_search_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_open",
+ "placesContext_open:newtab",
+ "placesContext_open:newcontainertab",
+ "placesContext_open:newwindow",
+ "placesContext_open:newprivatewindow",
+ "placesContext_showInFolder",
+ "placesContext_deleteBookmark",
+ "placesContext_cut",
+ "placesContext_copy",
+ ];
+ if (!userContextEnabled) {
+ optionItems.splice(
+ optionItems.indexOf("placesContext_open:newcontainertab"),
+ 1
+ );
+ }
+
+ await withLibraryWindow("BookmarksToolbar", async ({ left, right }) => {
+ await checkContextMenu(
+ async bookmark => {
+ info("Checking bookmark library menu contents in search context");
+ // Perform a search first
+ let searchBox = right.ownerDocument.getElementById("searchFilter");
+ searchBox.value = SECOND_BOOKMARK_TITLE;
+ searchBox.doCommand();
+
+ let contextMenu = right.ownerDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ right.selectItems([bookmark.guid]);
+ synthesizeClickOnSelectedTreeCell(right, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ right.ownerDocument
+ );
+ });
+});
+
+add_task(async function test_sidebar_mixedselection_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_delete",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ ];
+
+ await withSidebarTree("bookmarks", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ tree.selectItems([bookmark.guid, folder.guid]);
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_sidebar_multiple_bookmarks_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_openBookmarkLinks:tabs",
+ "placesContext_show_bookmark:info",
+ "placesContext_deleteBookmark",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ ];
+
+ await withSidebarTree("bookmarks", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ let bookmark2 = await PlacesUtils.bookmarks.insert({
+ url: "http://example.com/",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ tree.selectItems([bookmark.guid, bookmark2.guid]);
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_sidebar_multiple_links_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_openLinks:tabs",
+ "placesContext_delete_history",
+ "placesContext_copy",
+ "placesContext_createBookmark",
+ ];
+
+ await withSidebarTree("history", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ await PlacesTestUtils.addVisits([
+ "http://example-1.com/",
+ "http://example-2.com/",
+ ]);
+ // Sort by last visited.
+ tree.ownerDocument.getElementById("bylastvisited").doCommand();
+ tree.selectAll();
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_sidebar_mixed_bookmarks_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_delete",
+ "placesContext_cut",
+ "placesContext_copy",
+ "placesContext_paste_group",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ ];
+
+ await withSidebarTree("bookmarks", async tree => {
+ await checkContextMenu(
+ async bookmark => {
+ let folder = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ tree.selectItems([bookmark.guid, folder.guid]);
+
+ let contextMenu =
+ SidebarUI.browser.contentDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(tree, { type: "contextmenu" });
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ SidebarUI.browser.contentDocument
+ );
+ });
+});
+
+add_task(async function test_library_noselection_contextmenu_contents() {
+ let optionItems = [
+ "placesContext_openBookmarkContainer:tabs",
+ "placesContext_new:bookmark",
+ "placesContext_new:folder",
+ "placesContext_new:separator",
+ "placesContext_paste",
+ ];
+
+ await withLibraryWindow("BookmarksToolbar", async ({ left, right }) => {
+ await checkContextMenu(
+ async bookmark => {
+ let contextMenu = right.ownerDocument.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ right.selectItems([]);
+ EventUtils.synthesizeMouseAtCenter(
+ right.body,
+ { type: "contextmenu" },
+ right.ownerGlobal
+ );
+ await popupShownPromise;
+ return contextMenu;
+ },
+ optionItems,
+ right.ownerDocument
+ );
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js b/browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js
new file mode 100644
index 0000000000..71b947f7ac
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js
@@ -0,0 +1,172 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function () {
+ let mainFolder = await PlacesUtils.bookmarks.insert({
+ title: "mainFolder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ const selectedNodeComparator = {
+ equalTitle: itemNode => {
+ Assert.equal(
+ tree.selectedNode.title,
+ itemNode.title,
+ "Select expected title"
+ );
+ },
+ equalNode: itemNode => {
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ itemNode.guid,
+ "Selected the expected node"
+ );
+ },
+ equalType: itemType => {
+ Assert.equal(tree.selectedNode.type, itemType, "Correct type");
+ },
+
+ equalChildCount: childrenAmount => {
+ Assert.equal(
+ tree.selectedNode.childCount,
+ childrenAmount,
+ `${childrenAmount} children`
+ );
+ },
+ };
+ let urlType = Ci.nsINavHistoryResultNode.RESULT_TYPE_URI;
+
+ info("Create tree of: folderA => subFolderA => 3 bookmarkItems");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: mainFolder.guid,
+ children: [
+ {
+ title: "FolderA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "subFolderA",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "firstBM",
+ url: "http://example.com/1",
+ },
+ {
+ title: "secondBM",
+ url: "http://example.com/2",
+ },
+ {
+ title: "thirdBM",
+ url: "http://example.com/3",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ info("Sanity check folderA, subFolderA, bookmarkItems");
+ tree.selectItems([mainFolder.guid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalTitle(mainFolder);
+ selectedNodeComparator.equalChildCount(1);
+
+ let sourceFolder = tree.selectedNode.getChild(0);
+ tree.selectNode(sourceFolder);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalTitle(sourceFolder);
+ selectedNodeComparator.equalChildCount(1);
+
+ let subSourceFolder = tree.selectedNode.getChild(0);
+ tree.selectNode(subSourceFolder);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalTitle(subSourceFolder);
+ selectedNodeComparator.equalChildCount(3);
+
+ let bm_2 = tree.selectedNode.getChild(1);
+ tree.selectNode(bm_2);
+ selectedNodeComparator.equalTitle(bm_2);
+
+ info(
+ "Copy folder tree from sourceFolder (folderA, subFolderA, bookmarkItems)"
+ );
+ tree.selectNode(sourceFolder);
+ await promiseClipboard(() => {
+ tree.controller.copy();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ tree.selectItems([mainFolder.guid]);
+
+ info("Paste copy of folderA");
+ await tree.controller.paste();
+
+ info("Sanity check copy/paste operation - mainFolder has 2 children");
+ tree.selectItems([mainFolder.guid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalChildCount(2);
+
+ info("Sanity check copy of folderA");
+ let copySourceFolder = tree.selectedNode.getChild(1);
+ tree.selectNode(copySourceFolder);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalTitle(copySourceFolder);
+ selectedNodeComparator.equalChildCount(1);
+
+ info("Sanity check copy subFolderA");
+ let copySubSourceFolder = tree.selectedNode.getChild(0);
+ tree.selectNode(copySubSourceFolder);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalTitle(copySubSourceFolder);
+ selectedNodeComparator.equalChildCount(3);
+
+ info("Sanity check copy BookmarkItem");
+ let copyBm_1 = tree.selectedNode.getChild(0);
+ tree.selectNode(copyBm_1);
+ selectedNodeComparator.equalTitle(copyBm_1);
+
+ info("Undo copy operation");
+ await PlacesTransactions.undo();
+ tree.selectItems([mainFolder.guid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+
+ info("Sanity check undo operation - mainFolder has 1 child");
+ selectedNodeComparator.equalChildCount(1);
+
+ info("Redo copy operation");
+ await PlacesTransactions.redo();
+ tree.selectItems([mainFolder.guid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+
+ info("Sanity check redo operation - mainFolder has 2 children");
+ selectedNodeComparator.equalChildCount(2);
+
+ info("Sanity check copy of folderA");
+ copySourceFolder = tree.selectedNode.getChild(1);
+ tree.selectNode(copySourceFolder);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalChildCount(1);
+
+ info("Sanity check copy subFolderA");
+ copySubSourceFolder = tree.selectedNode.getChild(0);
+ tree.selectNode(copySubSourceFolder);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ selectedNodeComparator.equalTitle(copySubSourceFolder);
+ selectedNodeComparator.equalChildCount(3);
+
+ info("Sanity check copy BookmarkItem");
+ let copyBm_2 = tree.selectedNode.getChild(1);
+ tree.selectNode(copyBm_2);
+ selectedNodeComparator.equalTitle(copyBm_2);
+ selectedNodeComparator.equalType(urlType);
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
new file mode 100644
index 0000000000..d981aa4713
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_folder_moveability.js
@@ -0,0 +1,139 @@
+/* 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/. */
+
+"use strict";
+
+add_task(async function () {
+ let root = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ info("Test a regular folder");
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: root.guid,
+ title: "",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ tree.selectItems([folder.guid]);
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ folder.guid,
+ "Selected the expected node"
+ );
+ Assert.equal(tree.selectedNode.type, 6, "node is a folder");
+ Assert.ok(
+ tree.controller.canMoveNode(tree.selectedNode),
+ "can move regular folder node"
+ );
+
+ info("Test a folder shortcut");
+ let shortcut = await PlacesUtils.bookmarks.insert({
+ parentGuid: root.guid,
+ title: "bar",
+ url: `place:parent=${folder.guid}`,
+ });
+ tree.selectItems([shortcut.guid]);
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ shortcut.guid,
+ "Selected the expected node"
+ );
+ Assert.equal(tree.selectedNode.type, 9, "node is a folder shortcut");
+ Assert.equal(
+ PlacesUtils.getConcreteItemGuid(tree.selectedNode),
+ folder.guid,
+ "shortcut node guid and concrete guid match"
+ );
+ Assert.ok(
+ tree.controller.canMoveNode(tree.selectedNode),
+ "can move folder shortcut node"
+ );
+
+ info("Test a query");
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: root.guid,
+ title: "",
+ url: "http://foo.com",
+ });
+ tree.selectItems([bookmark.guid]);
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ bookmark.guid,
+ "Selected the expected node"
+ );
+ let query = await PlacesUtils.bookmarks.insert({
+ parentGuid: root.guid,
+ title: "bar",
+ url: `place:terms=foo`,
+ });
+ tree.selectItems([query.guid]);
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ query.guid,
+ "Selected the expected node"
+ );
+ Assert.ok(
+ tree.controller.canMoveNode(tree.selectedNode),
+ "can move query node"
+ );
+
+ info("Test a tag container");
+ PlacesUtils.tagging.tagURI(bookmark.url.URI, ["bar"]);
+ // Add the tags root query.
+ let tagsQuery = await PlacesUtils.bookmarks.insert({
+ parentGuid: root.guid,
+ title: "",
+ url: "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_TAGS_ROOT,
+ });
+ tree.selectItems([tagsQuery.guid]);
+ PlacesUtils.asQuery(tree.selectedNode).containerOpen = true;
+ Assert.equal(tree.selectedNode.childCount, 1, "has tags");
+ let tagNode = tree.selectedNode.getChild(0);
+ Assert.ok(
+ !tree.controller.canMoveNode(tagNode),
+ "should not be able to move tag container node"
+ );
+ tree.selectedNode.containerOpen = false;
+
+ info(
+ "Test that special folders and cannot be moved but other shortcuts can."
+ );
+ let roots = [
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ ];
+
+ for (let guid of roots) {
+ tree.selectItems([guid]);
+ Assert.ok(
+ !tree.controller.canMoveNode(tree.selectedNode),
+ "shouldn't be able to move default shortcuts to roots"
+ );
+ let s = await PlacesUtils.bookmarks.insert({
+ parentGuid: root.guid,
+ title: "bar",
+ url: `place:parent=${guid}`,
+ });
+ tree.selectItems([s.guid]);
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ s.guid,
+ "Selected the expected node"
+ );
+ Assert.ok(
+ tree.controller.canMoveNode(tree.selectedNode),
+ "should be able to move user-created shortcuts to roots"
+ );
+ }
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js b/browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js
new file mode 100644
index 0000000000..4e3d29cf21
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function onPopupEvent(popup, evt) {
+ let fullEvent = "popup" + evt;
+ return BrowserTestUtils.waitForEvent(popup, fullEvent, false, e => {
+ return e.target == popup;
+ });
+}
+
+add_task(async function test_bookmarks_menu() {
+ // On macOS, ctrl-click shouldn't open the panel because this normally opens
+ // the context menu. This happens via the `contextmenu` event which is created
+ // by widget code, so our simulated clicks do not do so, so we can't test
+ // anything on macOS.
+ if (AppConstants.platform == "macosx") {
+ ok(true, "The test is ignored on Mac");
+ return;
+ }
+
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ 4
+ );
+
+ const button = document.getElementById("bookmarks-menu-button");
+ const popup = document.getElementById("BMB_bookmarksPopup");
+
+ let shownPromise = onPopupEvent(popup, "shown");
+ EventUtils.synthesizeMouseAtCenter(button, { ctrlKey: true });
+ await shownPromise;
+ ok(true, "Bookmarks menu shown after button pressed");
+
+ // Close bookmarks popup.
+ let hiddenPromise = onPopupEvent(popup, "hidden");
+ popup.hidePopup();
+ await hiddenPromise;
+
+ CustomizableUI.reset();
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_popup.js b/browser/components/places/tests/browser/browser_bookmark_popup.js
new file mode 100644
index 0000000000..616755b7e1
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_popup.js
@@ -0,0 +1,712 @@
+/* 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/. */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+requestLongerTimeout(2);
+
+/**
+ * Test opening and closing the bookmarks panel.
+ */
+let win;
+let bookmarkPanel;
+let bookmarkStar;
+let bookmarkPanelTitle;
+let bookmarkRemoveButton;
+let editBookmarkPanelRemoveButtonRect;
+
+const TEST_URL = "data:text/html,<html><body></body></html>";
+
+add_setup(async function () {
+ win = await BrowserTestUtils.openNewBrowserWindow();
+
+ win.StarUI._createPanelIfNeeded();
+ win.StarUI._closePanelQuickForTesting = true;
+ bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+ bookmarkPanel.setAttribute("animate", false);
+ bookmarkStar = win.BookmarkingUI.star;
+ bookmarkPanelTitle = win.document.getElementById("editBookmarkPanelTitle");
+ bookmarkRemoveButton = win.document.getElementById(
+ "editBookmarkPanelRemoveButton"
+ );
+
+ registerCleanupFunction(async () => {
+ delete win.StarUI._closePanelQuickForTesting;
+ await BrowserTestUtils.closeWindow(win);
+ });
+});
+
+function mouseout() {
+ let mouseOutPromise = BrowserTestUtils.waitForEvent(
+ bookmarkPanel,
+ "mouseout"
+ );
+ EventUtils.synthesizeNativeMouseEvent({
+ type: "mousemove",
+ target: win.gURLBar.textbox,
+ offsetX: 0,
+ offsetY: 0,
+ win,
+ });
+ EventUtils.synthesizeMouse(bookmarkPanel, 0, 0, { type: "mouseout" }, win);
+ info("Waiting for mouseout event");
+ return mouseOutPromise;
+}
+
+async function test_bookmarks_popup({
+ isNewBookmark,
+ popupShowFn,
+ popupEditFn,
+ shouldAutoClose,
+ popupHideFn,
+ isBookmarkRemoved,
+}) {
+ await BrowserTestUtils.withNewTab(
+ { gBrowser: win.gBrowser, url: TEST_URL },
+ async function (browser) {
+ try {
+ if (!isNewBookmark) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: await PlacesUIUtils.defaultParentGuid,
+ url: TEST_URL,
+ title: "Home Page",
+ });
+ }
+
+ info(`BookmarkingUI.status is ${win.BookmarkingUI.status}`);
+ await TestUtils.waitForCondition(
+ () => win.BookmarkingUI.status != win.BookmarkingUI.STATUS_UPDATING,
+ "BookmarkingUI should not be updating"
+ );
+
+ Assert.equal(
+ bookmarkStar.hasAttribute("starred"),
+ !isNewBookmark,
+ "Page should only be starred prior to popupshown if editing bookmark"
+ );
+ Assert.equal(
+ bookmarkPanel.state,
+ "closed",
+ "Panel should be 'closed' to start test"
+ );
+ let shownPromise = promisePopupShown(bookmarkPanel);
+ await popupShowFn(browser);
+ await shownPromise;
+ Assert.equal(
+ bookmarkPanel.state,
+ "open",
+ "Panel should be 'open' after shownPromise is resolved"
+ );
+
+ editBookmarkPanelRemoveButtonRect =
+ bookmarkRemoveButton.getBoundingClientRect();
+
+ if (popupEditFn) {
+ await popupEditFn();
+ }
+ Assert.equal(
+ bookmarkStar.getAttribute("starred"),
+ "true",
+ "Page is starred"
+ );
+ Assert.equal(
+ bookmarkPanelTitle.dataset.l10nId,
+ isNewBookmark ? "bookmarks-add-bookmark" : "bookmarks-edit-bookmark",
+ "title should match isEditingBookmark state"
+ );
+ Assert.equal(
+ bookmarkRemoveButton.dataset.l10nId,
+ isNewBookmark ? "bookmark-panel-cancel" : "bookmark-panel-remove",
+ "remove/cancel button label should match isEditingBookmark state"
+ );
+
+ if (!shouldAutoClose) {
+ await new Promise(resolve => setTimeout(resolve, 400));
+ Assert.equal(
+ bookmarkPanel.state,
+ "open",
+ "Panel should still be 'open' for non-autoclose"
+ );
+ }
+
+ let defaultLocation = await PlacesUIUtils.defaultParentGuid;
+ const promises = [];
+ if (isNewBookmark && !isBookmarkRemoved) {
+ // Expect new bookmark to be created.
+ promises.push(
+ PlacesTestUtils.waitForNotification("bookmark-added", events =>
+ events.some(
+ ({ parentGuid, url }) =>
+ parentGuid == defaultLocation && TEST_URL == url
+ )
+ )
+ );
+ }
+ if (!isNewBookmark && isBookmarkRemoved) {
+ // Expect existing bookmark to be removed.
+ promises.push(
+ PlacesTestUtils.waitForNotification("bookmark-removed", events =>
+ events.some(
+ ({ parentGuid, url }) =>
+ parentGuid == defaultLocation && TEST_URL == url
+ )
+ )
+ );
+ }
+
+ promises.push(promisePopupHidden(bookmarkPanel));
+ if (popupHideFn) {
+ await popupHideFn();
+ } else {
+ // Move the mouse out of the way so that the panel will auto-close.
+ await mouseout();
+ }
+ await Promise.all(promises);
+
+ Assert.equal(
+ bookmarkStar.hasAttribute("starred"),
+ !isBookmarkRemoved,
+ "Page is starred after closing"
+ );
+
+ // Count number of bookmarks.
+ let count = 0;
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, () => count++);
+ const message = isBookmarkRemoved
+ ? "No bookmark should exist"
+ : "Only one bookmark should exist";
+ Assert.equal(count, isBookmarkRemoved ? 0 : 1, message);
+ } finally {
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url: TEST_URL });
+ Assert.equal(
+ !!bookmark,
+ !isBookmarkRemoved,
+ "bookmark should not be present if a panel action should've removed it"
+ );
+ if (bookmark) {
+ await PlacesUtils.bookmarks.remove(bookmark);
+ }
+ }
+ }
+ );
+}
+
+add_task(async function panel_shown_for_new_bookmarks_and_autocloses() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(
+ async function panel_shown_once_for_doubleclick_on_new_bookmark_star_and_autocloses() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ EventUtils.synthesizeMouse(
+ bookmarkStar,
+ 10,
+ 10,
+ { clickCount: 2 },
+ win
+ );
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+ }
+);
+
+add_task(
+ async function panel_shown_once_for_slow_doubleclick_on_new_bookmark_star_and_autocloses() {
+ todo(
+ false,
+ "bug 1250267, may need to add some tracking state to " +
+ "browser-places.js for this."
+ );
+
+ /*
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ *popupShowFn() {
+ EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window);
+ await new Promise(resolve => setTimeout(resolve, 300));
+ EventUtils.synthesizeMouse(bookmarkStar, 10, 10, window);
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+ */
+ }
+);
+
+add_task(
+ async function panel_shown_for_keyboardshortcut_on_new_bookmark_star_and_autocloses() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ EventUtils.synthesizeKey("D", { accelKey: true }, win);
+ },
+ shouldAutoClose: true,
+ isBookmarkRemoved: false,
+ });
+ }
+);
+
+add_task(async function panel_shown_for_new_bookmarks_mousemove_mouseout() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ async popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(
+ bookmarkPanel,
+ "mousemove"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ bookmarkPanel,
+ { type: "mousemove" },
+ win
+ );
+ info("Waiting for mousemove event");
+ await mouseMovePromise;
+ info("Got mousemove event");
+
+ await new Promise(resolve => setTimeout(resolve, 400));
+ is(
+ bookmarkPanel.state,
+ "open",
+ "Panel should still be open on mousemove"
+ );
+ },
+ async popupHideFn() {
+ await mouseout();
+ info("Got mouseout event, should autoclose now");
+ },
+ shouldAutoClose: false,
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(async function panel_shown_for_new_bookmark_close_with_ESC() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ shouldAutoClose: true,
+ popupHideFn() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(async function panel_shown_for_editing_no_autoclose_close_with_ESC() {
+ await test_bookmarks_popup({
+ isNewBookmark: false,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, win);
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(async function panel_shown_for_new_bookmark_keypress_no_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ popupEditFn() {
+ EventUtils.sendChar("VK_TAB", win);
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(async function bookmark_with_invalid_default_folder() {
+ await createAndRemoveDefaultFolder();
+
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ shouldAutoClose: true,
+ async popupShowFn(browser) {
+ EventUtils.synthesizeKey("d", { accelKey: true }, win);
+ },
+ });
+});
+
+add_task(
+ async function panel_shown_for_new_bookmark_compositionstart_no_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ async popupEditFn() {
+ let compositionStartPromise = BrowserTestUtils.waitForEvent(
+ bookmarkPanel,
+ "compositionstart"
+ );
+ EventUtils.synthesizeComposition({ type: "compositionstart" }, win);
+ info("Waiting for compositionstart event");
+ await compositionStartPromise;
+ info("Got compositionstart event");
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeComposition(
+ { type: "compositioncommitasis" },
+ win
+ );
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+ }
+);
+
+add_task(
+ async function panel_shown_for_new_bookmark_compositionstart_mouseout_no_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ async popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(
+ bookmarkPanel,
+ "mousemove"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ bookmarkPanel,
+ {
+ type: "mousemove",
+ },
+ win
+ );
+ info("Waiting for mousemove event");
+ await mouseMovePromise;
+ info("Got mousemove event");
+
+ let compositionStartPromise = BrowserTestUtils.waitForEvent(
+ bookmarkPanel,
+ "compositionstart"
+ );
+ EventUtils.synthesizeComposition({ type: "compositionstart" }, win);
+ info("Waiting for compositionstart event");
+ await compositionStartPromise;
+ info("Got compositionstart event");
+
+ await mouseout();
+ info("Got mouseout event, but shouldn't run autoclose");
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeComposition(
+ { type: "compositioncommitasis" },
+ win
+ );
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+ }
+);
+
+add_task(
+ async function panel_shown_for_new_bookmark_compositionend_no_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn() {
+ bookmarkStar.click();
+ },
+ async popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(
+ bookmarkPanel,
+ "mousemove"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ bookmarkPanel,
+ {
+ type: "mousemove",
+ },
+ win
+ );
+ info("Waiting for mousemove event");
+ await mouseMovePromise;
+ info("Got mousemove event");
+
+ EventUtils.synthesizeComposition(
+ {
+ type: "compositioncommit",
+ data: "committed text",
+ },
+ win
+ );
+ },
+ popupHideFn() {
+ bookmarkPanel.hidePopup();
+ },
+ shouldAutoClose: false,
+ isBookmarkRemoved: false,
+ });
+ }
+);
+
+add_task(async function contextmenu_new_bookmark_keypress_no_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ async popupShowFn(browser) {
+ let contextMenu = win.document.getElementById("contentAreaContextMenu");
+ let awaitPopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ let awaitPopupHidden = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ browser
+ );
+ await awaitPopupShown;
+ contextMenu.activateItem(
+ win.document.getElementById("context-bookmarkpage")
+ );
+ await awaitPopupHidden;
+ },
+ popupEditFn() {
+ EventUtils.sendChar("VK_TAB", win);
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ bookmarkPanel.hidePopup();
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(async function bookmarks_menu_new_bookmark_remove_bookmark() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn(browser) {
+ win.document.getElementById("menu_bookmarkThisPage").doCommand();
+ },
+ shouldAutoClose: true,
+ popupHideFn() {
+ win.document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(async function ctrl_d_edit_bookmark_remove_bookmark() {
+ await test_bookmarks_popup({
+ isNewBookmark: false,
+ popupShowFn(browser) {
+ EventUtils.synthesizeKey("D", { accelKey: true }, win);
+ },
+ shouldAutoClose: true,
+ popupHideFn() {
+ win.document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(async function enter_on_remove_bookmark_should_remove_bookmark() {
+ if (AppConstants.platform == "macosx") {
+ // "Full Keyboard Access" is disabled by default, and thus doesn't allow
+ // keyboard navigation to the "Remove Bookmarks" button by default.
+ return;
+ }
+
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn(browser) {
+ EventUtils.synthesizeKey("D", { accelKey: true }, win);
+ },
+ shouldAutoClose: true,
+ popupHideFn() {
+ while (
+ !win.document.activeElement ||
+ win.document.activeElement.id != "editBookmarkPanelRemoveButton"
+ ) {
+ EventUtils.sendChar("VK_TAB", win);
+ }
+ EventUtils.sendChar("VK_RETURN", win);
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(async function mouse_hovering_panel_should_prevent_autoclose() {
+ if (AppConstants.platform != "win") {
+ // This test requires synthesizing native mouse movement which is
+ // best supported on Windows.
+ return;
+ }
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ async popupShowFn() {
+ await EventUtils.promiseNativeMouseEvent({
+ type: "mousemove",
+ target: win.document.documentElement,
+ offsetX: editBookmarkPanelRemoveButtonRect.left,
+ offsetY: editBookmarkPanelRemoveButtonRect.top,
+ });
+ EventUtils.synthesizeKey("D", { accelKey: true }, win);
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ win.document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(async function ctrl_d_new_bookmark_mousedown_mouseout_no_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: true,
+ popupShowFn(browser) {
+ EventUtils.synthesizeKey("D", { accelKey: true }, win);
+ },
+ async popupEditFn() {
+ let mouseMovePromise = BrowserTestUtils.waitForEvent(
+ bookmarkPanel,
+ "mousemove"
+ );
+ EventUtils.synthesizeMouseAtCenter(
+ bookmarkPanel,
+ { type: "mousemove" },
+ win
+ );
+ info("Waiting for mousemove event");
+ await mouseMovePromise;
+ info("Got mousemove event");
+
+ await new Promise(resolve => setTimeout(resolve, 400));
+ is(
+ bookmarkPanel.state,
+ "open",
+ "Panel should still be open on mousemove"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(
+ bookmarkPanelTitle,
+ {
+ button: 1,
+ type: "mousedown",
+ },
+ win
+ );
+
+ await mouseout();
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ win.document.getElementById("editBookmarkPanelRemoveButton").click();
+ },
+ isBookmarkRemoved: true,
+ });
+});
+
+add_task(async function enter_during_autocomplete_should_prevent_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: false,
+ async popupShowFn(browser) {
+ PlacesUtils.tagging.tagURI(makeURI(TEST_URL), ["Abc"]);
+ EventUtils.synthesizeKey("d", { accelKey: true }, win);
+ },
+ async popupEditFn() {
+ // Start autocomplete with the registered tag.
+ let tagsField = win.document.getElementById("editBMPanel_tagsField");
+ tagsField.value = "";
+ let popup = win.document.getElementById("editBMPanel_tagsAutocomplete");
+ let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ tagsField.focus();
+ EventUtils.sendString("a", win);
+ await promiseShown;
+ ok(promiseShown, "autocomplete shown");
+
+ // Select first candidate.
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+
+ // Type Enter key to choose the item.
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+
+ Assert.equal(
+ tagsField.value,
+ "Abc",
+ "Autocomplete should've inserted the selected item"
+ );
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+ },
+ isBookmarkRemoved: false,
+ });
+});
+
+add_task(async function escape_during_autocomplete_should_prevent_autoclose() {
+ await test_bookmarks_popup({
+ isNewBookmark: false,
+ async popupShowFn(browser) {
+ PlacesUtils.tagging.tagURI(makeURI(TEST_URL), ["Abc"]);
+ EventUtils.synthesizeKey("d", { accelKey: true }, win);
+ },
+ async popupEditFn() {
+ // Start autocomplete with the registered tag.
+ let tagsField = win.document.getElementById("editBMPanel_tagsField");
+ tagsField.value = "";
+ let popup = win.document.getElementById("editBMPanel_tagsAutocomplete");
+ let promiseShown = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ tagsField.focus();
+ EventUtils.sendString("a", win);
+ await promiseShown;
+ ok(promiseShown, "autocomplete shown");
+
+ // Select first candidate.
+ EventUtils.synthesizeKey("KEY_ArrowDown", {}, win);
+
+ // Type Escape key to close autocomplete.
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+
+ // The text reverts to what was typed.
+ // Note, it's important that this is different from the previously
+ // inserted tag, since it will test an untag/tag undo condition.
+ Assert.equal(
+ tagsField.value,
+ "a",
+ "Autocomplete should revert to what was typed"
+ );
+ },
+ shouldAutoClose: false,
+ popupHideFn() {
+ EventUtils.synthesizeKey("KEY_Escape", {}, win);
+ },
+ isBookmarkRemoved: false,
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_private_window.js b/browser/components/places/tests/browser/browser_bookmark_private_window.js
new file mode 100644
index 0000000000..2557833816
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_private_window.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that the a website can be bookmarked from a private window.
+ */
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+
+// Cleanup.
+registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_add_bookmark_from_private_window() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+ let tab = await BrowserTestUtils.openNewForegroundTab(win.gBrowser, TEST_URL);
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ // Open the bookmark panel.
+ win.StarUI._createPanelIfNeeded();
+ let bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+ let shownPromise = promisePopupShown(bookmarkPanel);
+ let bookmarkAddedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(({ url }) => url === TEST_URL)
+ );
+ let bookmarkStar = win.BookmarkingUI.star;
+ bookmarkStar.click();
+ await shownPromise;
+
+ // Check if the bookmark star changes its state after click.
+ Assert.equal(
+ bookmarkStar.getAttribute("starred"),
+ "true",
+ "Bookmark star changed its state correctly."
+ );
+
+ // Close the bookmark panel.
+ let hiddenPromise = promisePopupHidden(bookmarkPanel);
+ let doneButton = win.document.getElementById("editBookmarkPanelDoneButton");
+ doneButton.click();
+ await hiddenPromise;
+ await bookmarkAddedPromise;
+
+ let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL });
+ Assert.equal(
+ bm.url,
+ TEST_URL,
+ "The bookmark was successfully saved in the database."
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_remove_tags.js b/browser/components/places/tests/browser/browser_bookmark_remove_tags.js
new file mode 100644
index 0000000000..ece5fdbce0
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_remove_tags.js
@@ -0,0 +1,256 @@
+/**
+ * Tests that the bookmark tags can be removed from the bookmark star, toolbar and sidebar.
+ */
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+const TEST_URI = Services.io.newURI(TEST_URL);
+const TEST_TAG = "tag";
+
+// Setup.
+add_setup(async function () {
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ // Cleanup.
+ registerCleanupFunction(async () => {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_remove_tags_from_BookmarkStar() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: TEST_URL,
+ });
+ PlacesUtils.tagging.tagURI(TEST_URI, ["tag1", "tag2", "tag3", "tag4"]);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ });
+
+ StarUI._createPanelIfNeeded();
+ await clickBookmarkStar();
+
+ // Check if the "Edit This Bookmark" panel is open.
+ let bookmarkPanelTitle = document.getElementById("editBookmarkPanelTitle");
+ Assert.equal(
+ document.l10n.getAttributes(bookmarkPanelTitle).id,
+ "bookmarks-edit-bookmark",
+ "Bookmark panel title is correct."
+ );
+
+ let promiseTagsChange = PlacesTestUtils.waitForNotification(
+ "bookmark-tags-changed"
+ );
+
+ // Update the "tags" field.
+ fillBookmarkTextField("editBMPanel_tagsField", "tag1, tag2, tag3", window);
+ let tagspicker = document.getElementById("editBMPanel_tagsField");
+ await TestUtils.waitForCondition(
+ () => tagspicker.value === "tag1, tag2, tag3",
+ "Tags are correct after update."
+ );
+
+ let doneButton = document.getElementById("editBookmarkPanelDoneButton");
+ doneButton.click();
+ await promiseTagsChange;
+
+ let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI);
+ Assert.deepEqual(
+ tags,
+ ["tag1", "tag2", "tag3"],
+ "Should have updated the bookmark tags in the database."
+ );
+});
+
+add_task(async function test_remove_tags_from_Toolbar() {
+ let toolbarBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: TEST_URL,
+ url: TEST_URL,
+ });
+
+ let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid);
+
+ await withBookmarksDialog(
+ false,
+ async function openPropertiesDialog() {
+ let placesContext = document.getElementById("placesContext");
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await promisePopup;
+
+ let properties = document.getElementById(
+ "placesContext_show_bookmark:info"
+ );
+ placesContext.activateItem(properties, {});
+ },
+ async function test(dialogWin) {
+ let tagspicker = dialogWin.document.getElementById(
+ "editBMPanel_tagsField"
+ );
+ Assert.equal(
+ tagspicker.value,
+ "tag1, tag2, tag3",
+ "Tags are correct before update."
+ );
+
+ let promiseTagsChange = PlacesTestUtils.waitForNotification(
+ "bookmark-tags-changed"
+ );
+
+ // Update the "tags" field.
+ fillBookmarkTextField(
+ "editBMPanel_tagsField",
+ "tag1, tag2",
+ dialogWin,
+ false
+ );
+ await TestUtils.waitForCondition(
+ () => tagspicker.value === "tag1, tag2",
+ "Tags are correct after update."
+ );
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ await promiseTagsChange;
+
+ let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI);
+ Assert.deepEqual(
+ tags,
+ ["tag1", "tag2"],
+ "Should have updated the bookmark tags in the database."
+ );
+ }
+ );
+});
+
+add_task(async function test_remove_tags_from_Sidebar() {
+ let bookmarks = [];
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm =>
+ bookmarks.push(bm)
+ );
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ tree.selectItems([bookmarks[0].guid]);
+
+ await withBookmarksDialog(
+ false,
+ function openPropertiesDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ let tagspicker = dialogWin.document.getElementById(
+ "editBMPanel_tagsField"
+ );
+ Assert.equal(
+ tagspicker.value,
+ "tag1, tag2",
+ "Tags are correct before update."
+ );
+
+ let promiseTagsChange = PlacesTestUtils.waitForNotification(
+ "bookmark-tags-changed"
+ );
+
+ // Update the "tags" field.
+ fillBookmarkTextField(
+ "editBMPanel_tagsField",
+ "tag1",
+ dialogWin,
+ false
+ );
+ await TestUtils.waitForCondition(
+ () => tagspicker.value === "tag1",
+ "Tags are correct after update."
+ );
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ await promiseTagsChange;
+
+ let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI);
+ Assert.deepEqual(
+ tags,
+ ["tag1"],
+ "Should have updated the bookmark tags in the database."
+ );
+ }
+ );
+ });
+});
+
+add_task(async function test_remove_tags_from_Library() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: TEST_URL,
+ });
+ PlacesUtils.tagging.tagURI(TEST_URI, [TEST_TAG]);
+ const getTags = () => PlacesUtils.tagging.getTagsForURI(TEST_URI);
+
+ // Open the Library and select the tag.
+ const library = await promiseLibrary("place:tag=" + TEST_TAG);
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ });
+
+ const contextMenu = library.document.getElementById("placesContext");
+ const contextMenuDeleteTag = library.document.getElementById(
+ "placesContext_removeTag"
+ );
+
+ let firstColumn = library.ContentTree.view.columns[0];
+ let firstBookmarkRect = library.ContentTree.view.getCoordsForCellItem(
+ 0,
+ firstColumn,
+ "bm0"
+ );
+
+ EventUtils.synthesizeMouse(
+ library.ContentTree.view.body,
+ firstBookmarkRect.x,
+ firstBookmarkRect.y,
+ { type: "contextmenu", button: 2 },
+ library
+ );
+
+ await BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+
+ ok(getTags().includes(TEST_TAG), "Test tag exists before delete.");
+
+ contextMenu.activateItem(contextMenuDeleteTag, {});
+
+ await PlacesTestUtils.waitForNotification("bookmark-tags-changed");
+ await promiseLibraryClosed(library);
+
+ ok(
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }),
+ "Bookmark still exists after removing tag."
+ );
+ ok(!getTags().includes(TEST_TAG), "Test tag is removed after delete.");
+});
diff --git a/browser/components/places/tests/browser/browser_bookmark_titles.js b/browser/components/places/tests/browser/browser_bookmark_titles.js
new file mode 100644
index 0000000000..c91f9559e7
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmark_titles.js
@@ -0,0 +1,129 @@
+/* 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 file is tests for the default titles that new bookmarks get.
+
+var tests = [
+ // Common page.
+ {
+ url: "http://example.com/browser/browser/components/places/tests/browser/dummy_page.html",
+ title: "Dummy test page",
+ isError: false,
+ },
+ // Data URI.
+ {
+ url: "data:text/html;charset=utf-8,<title>test%20data:%20url</title>",
+ title: "test data: url",
+ isError: false,
+ },
+ // about:neterror
+ {
+ url: "data:application/xhtml+xml,",
+ title: "data:application/xhtml+xml,",
+ isError: true,
+ },
+ // about:certerror
+ {
+ url: "https://untrusted.example.com/somepage.html",
+ title: "https://untrusted.example.com/somepage.html",
+ isError: true,
+ },
+];
+
+SpecialPowers.pushPrefEnv({
+ set: [["browser.bookmarks.editDialog.showForNewBookmarks", false]],
+});
+
+add_task(async function check_default_bookmark_title() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://www.example.com/"
+ );
+ let browser = tab.linkedBrowser;
+
+ // Test that a bookmark of each URI gets the corresponding default title.
+ for (let { url, title, isError } of tests) {
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ url,
+ isError
+ );
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await promiseLoaded;
+
+ await checkBookmark(url, title);
+ }
+
+ // Network failure test: now that dummy_page.html is in history, bookmarking
+ // it should give the last known page title as the default bookmark title.
+
+ // Simulate a network outage with offline mode. (Localhost is still
+ // accessible in offline mode, so disable the test proxy as well.)
+ BrowserOffline.toggleOfflineStatus();
+ let proxy = Services.prefs.getIntPref("network.proxy.type");
+ Services.prefs.setIntPref("network.proxy.type", 0);
+ registerCleanupFunction(function () {
+ BrowserOffline.toggleOfflineStatus();
+ Services.prefs.setIntPref("network.proxy.type", proxy);
+ });
+
+ // LOAD_FLAGS_BYPASS_CACHE isn't good enough. So clear the cache.
+ Services.cache2.clear();
+
+ let { url, title } = tests[0];
+
+ let promiseLoaded = BrowserTestUtils.browserLoaded(
+ browser,
+ false,
+ null,
+ true
+ );
+ BrowserTestUtils.startLoadingURIString(browser, url);
+ await promiseLoaded;
+
+ // The offline mode test is only good if the page failed to load.
+ await SpecialPowers.spawn(browser, [], function () {
+ Assert.equal(
+ content.document.documentURI.substring(0, 14),
+ "about:neterror",
+ "Offline mode successfully simulated network outage."
+ );
+ });
+ await checkBookmark(url, title);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Bookmark the current page and confirm that the new bookmark has the expected
+// title. (Then delete the bookmark.)
+async function checkBookmark(url, expected_title) {
+ Assert.equal(
+ gBrowser.selectedBrowser.currentURI.spec,
+ url,
+ "Trying to bookmark the expected uri"
+ );
+
+ let promiseBookmark = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events =>
+ events.some(
+ ({ url: eventUrl }) =>
+ eventUrl == gBrowser.selectedBrowser.currentURI.spec
+ )
+ );
+ PlacesCommandHook.bookmarkPage();
+ await promiseBookmark;
+
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url });
+
+ Assert.ok(bookmark, "Found the expected bookmark");
+ Assert.equal(
+ bookmark.title,
+ expected_title,
+ "Bookmark got a good default title."
+ );
+
+ await PlacesUtils.bookmarks.remove(bookmark);
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js
new file mode 100644
index 0000000000..69f6298b8c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js
@@ -0,0 +1,79 @@
+"use strict";
+
+let BASE_URL = getRootDirectory(gTestPath).replace(
+ "chrome://mochitests/content/",
+ "https://example.com/"
+);
+const TEST_URL = BASE_URL + "pageopeningwindow.html";
+const DUMMY_URL = BASE_URL + "bookmarklet_windowOpen_dummy.html";
+
+function makeBookmarkFor(url, keyword) {
+ return Promise.all([
+ PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bookmarklet",
+ url,
+ }),
+ PlacesUtils.keywords.insert({ url, keyword }),
+ ]);
+}
+
+add_task(async function openKeywordBookmarkWithWindowOpen() {
+ // This is the current default, but let's not assume that...
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.link.open_newwindow", 3],
+ ["dom.disable_open_during_load", true],
+ ],
+ });
+
+ let moztab;
+ let tabOpened = BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ DUMMY_URL
+ ).then(tab => {
+ moztab = tab;
+ });
+ let keywordForBM = "openmeatab";
+
+ let bookmarkInfo;
+ let bookmarkCreated = makeBookmarkFor(
+ "javascript:void open('" + TEST_URL + "')",
+ keywordForBM
+ ).then(values => {
+ bookmarkInfo = values[0];
+ });
+ await Promise.all([tabOpened, bookmarkCreated]);
+
+ registerCleanupFunction(function () {
+ return Promise.all([
+ PlacesUtils.bookmarks.remove(bookmarkInfo),
+ PlacesUtils.keywords.remove(keywordForBM),
+ ]);
+ });
+ gURLBar.value = keywordForBM;
+ gURLBar.focus();
+
+ let tabCreatedPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.tabContainer,
+ "TabOpen"
+ );
+ EventUtils.synthesizeKey("KEY_Enter");
+
+ info("Waiting for tab being created");
+ let { target: tab } = await tabCreatedPromise;
+ info("Got tab");
+ let browser = tab.linkedBrowser;
+ if (!browser.currentURI || browser.currentURI.spec != TEST_URL) {
+ info("Waiting for browser load");
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
+ }
+ is(
+ browser.currentURI && browser.currentURI.spec,
+ TEST_URL,
+ "Tab with expected URL loaded."
+ );
+ info("Waiting to remove tab");
+ BrowserTestUtils.removeTab(tab);
+ BrowserTestUtils.removeTab(moztab);
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarksProperties.js b/browser/components/places/tests/browser/browser_bookmarksProperties.js
new file mode 100644
index 0000000000..8f1d783a49
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js
@@ -0,0 +1,532 @@
+/* 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 the bookmarks Properties dialog.
+ */
+
+// DOM ids of Places sidebar trees.
+const SIDEBAR_HISTORY_TREE_ID = "historyTree";
+const SIDEBAR_BOOKMARKS_TREE_ID = "bookmarks-view";
+
+const SIDEBAR_HISTORY_ID = "viewHistorySidebar";
+const SIDEBAR_BOOKMARKS_ID = "viewBookmarksSidebar";
+
+// For history sidebar.
+const SIDEBAR_HISTORY_BYLASTVISITED_VIEW = "bylastvisited";
+const SIDEBAR_HISTORY_BYMOSTVISITED_VIEW = "byvisited";
+const SIDEBAR_HISTORY_BYDATE_VIEW = "byday";
+const SIDEBAR_HISTORY_BYSITE_VIEW = "bysite";
+const SIDEBAR_HISTORY_BYDATEANDSITE_VIEW = "bydateandsite";
+
+// Action to execute on the current node.
+const ACTION_EDIT = 0;
+const ACTION_ADD = 1;
+
+// If action is ACTION_ADD, set type to one of those, to define what do you
+// want to create.
+const TYPE_FOLDER = 0;
+const TYPE_BOOKMARK = 1;
+
+const TEST_URL = "http://www.example.com/";
+
+const DIALOG_URL = "chrome://browser/content/places/bookmarkProperties.xhtml";
+
+function add_bookmark(url) {
+ return PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ title: `bookmark/${url}`,
+ });
+}
+
+// Each test is an obj w/ a desc property and run method.
+var gTests = [];
+
+// ------------------------------------------------------------------------------
+// Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog
+
+gTests.push({
+ desc: "Bug 462662 - Pressing Enter to select tag from autocomplete closes bookmarks properties dialog",
+ sidebar: SIDEBAR_BOOKMARKS_ID,
+ action: ACTION_EDIT,
+ itemType: null,
+ window: null,
+ _bookmark: null,
+ _cleanShutdown: false,
+
+ async setup() {
+ // Add a bookmark in unsorted bookmarks folder.
+ this._bookmark = await add_bookmark(TEST_URL);
+ Assert.ok(this._bookmark, "Correctly added a bookmark");
+
+ // Add a tag to this bookmark.
+ PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["testTag"]);
+ var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
+ Assert.equal(tags[0], "testTag", "Correctly added a tag");
+ },
+
+ selectNode(tree) {
+ tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ tree.selectItems([this._bookmark.guid]);
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ this._bookmark.guid,
+ "Bookmark has been selected"
+ );
+ },
+
+ async run() {
+ // open tags autocomplete and press enter
+ var tagsField = this.window.document.getElementById(
+ "editBMPanel_tagsField"
+ );
+ var self = this;
+
+ let unloadPromise = new Promise(resolve => {
+ this.window.addEventListener(
+ "unload",
+ function (event) {
+ tagsField.popup.removeEventListener(
+ "popuphidden",
+ popupListener,
+ true
+ );
+ Assert.ok(
+ self._cleanShutdown,
+ "Dialog window should not be closed by pressing Enter on the autocomplete popup"
+ );
+ executeSoon(function () {
+ resolve();
+ });
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ var popupListener = {
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "popuphidden":
+ // Everything worked fine, we can stop observing the window.
+ self._cleanShutdown = true;
+ self.window.document
+ .getElementById("bookmarkpropertiesdialog")
+ .cancelDialog();
+ break;
+ case "popupshown":
+ tagsField.popup.removeEventListener("popupshown", this, true);
+ // In case this test fails the window will close, the test will fail
+ // since we didn't set _cleanShutdown.
+ let richlistbox = tagsField.popup.richlistbox;
+ // Focus and select first result.
+ Assert.equal(
+ richlistbox.itemCount,
+ 1,
+ "We have 1 autocomplete result"
+ );
+ tagsField.popup.selectedIndex = 0;
+ Assert.equal(
+ richlistbox.selectedItems.length,
+ 1,
+ "We have selected a tag from the autocomplete popup"
+ );
+ info("About to focus the autocomplete results");
+ richlistbox.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, self.window);
+ break;
+ default:
+ Assert.ok(false, "unknown event: " + aEvent.type);
+ }
+ },
+ };
+ tagsField.popup.addEventListener("popupshown", popupListener, true);
+ tagsField.popup.addEventListener("popuphidden", popupListener, true);
+
+ // Open tags autocomplete popup.
+ info("About to focus the tagsField");
+ executeSoon(() => {
+ tagsField.focus();
+ tagsField.value = "";
+ EventUtils.synthesizeKey("t", {}, this.window);
+ });
+ await unloadPromise;
+ },
+
+ finish() {
+ SidebarUI.hide();
+ },
+
+ async cleanup() {
+ // Check tags have not changed.
+ var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
+ Assert.equal(tags[0], "testTag", "Tag on node has not changed");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["testTag"]);
+ await PlacesUtils.bookmarks.remove(this._bookmark);
+ let bm = await PlacesUtils.bookmarks.fetch(this._bookmark.guid);
+ Assert.ok(!bm, "should have been removed");
+ },
+});
+
+// ------------------------------------------------------------------------------
+// Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel
+
+gTests.push({
+ desc: "Bug 476020 - Pressing Esc while having the tag autocomplete open closes the bookmarks panel",
+ sidebar: SIDEBAR_BOOKMARKS_ID,
+ action: ACTION_EDIT,
+ itemType: null,
+ window: null,
+ _bookmark: null,
+ _cleanShutdown: false,
+
+ async setup() {
+ // Add a bookmark in unsorted bookmarks folder.
+ this._bookmark = await add_bookmark(TEST_URL);
+ Assert.ok(this._bookmark, "Correctly added a bookmark");
+
+ // Add a tag to this bookmark.
+ PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["testTag"]);
+ var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
+ Assert.equal(tags[0], "testTag", "Correctly added a tag");
+ },
+
+ selectNode(tree) {
+ tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ tree.selectItems([this._bookmark.guid]);
+ Assert.equal(
+ tree.selectedNode.bookmarkGuid,
+ this._bookmark.guid,
+ "Bookmark has been selected"
+ );
+ },
+
+ async run() {
+ // open tags autocomplete and press enter
+ var tagsField = this.window.document.getElementById(
+ "editBMPanel_tagsField"
+ );
+ var self = this;
+
+ let hiddenPromise = new Promise(resolve => {
+ this.window.addEventListener(
+ "unload",
+ function (event) {
+ tagsField.popup.removeEventListener(
+ "popuphidden",
+ popupListener,
+ true
+ );
+ Assert.ok(
+ self._cleanShutdown,
+ "Dialog window should not be closed by pressing Escape on the autocomplete popup"
+ );
+ executeSoon(function () {
+ resolve();
+ });
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ var popupListener = {
+ handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "popuphidden":
+ // Everything worked fine.
+ self._cleanShutdown = true;
+ self.window.document
+ .getElementById("bookmarkpropertiesdialog")
+ .cancelDialog();
+ break;
+ case "popupshown":
+ tagsField.popup.removeEventListener("popupshown", this, true);
+ // In case this test fails the window will close, the test will fail
+ // since we didn't set _cleanShutdown.
+ let richlistbox = tagsField.popup.richlistbox;
+ // Focus and select first result.
+ Assert.equal(
+ richlistbox.itemCount,
+ 1,
+ "We have 1 autocomplete result"
+ );
+ tagsField.popup.selectedIndex = 0;
+ Assert.equal(
+ richlistbox.selectedItems.length,
+ 1,
+ "We have selected a tag from the autocomplete popup"
+ );
+ info("About to focus the autocomplete results");
+ richlistbox.focus();
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
+ break;
+ default:
+ Assert.ok(false, "unknown event: " + aEvent.type);
+ }
+ },
+ };
+ tagsField.popup.addEventListener("popupshown", popupListener, true);
+ tagsField.popup.addEventListener("popuphidden", popupListener, true);
+
+ // Open tags autocomplete popup.
+ info("About to focus the tagsField");
+ tagsField.focus();
+ tagsField.value = "";
+ EventUtils.synthesizeKey("t", {}, this.window);
+ await hiddenPromise;
+ },
+
+ finish() {
+ SidebarUI.hide();
+ },
+
+ async cleanup() {
+ // Check tags have not changed.
+ var tags = PlacesUtils.tagging.getTagsForURI(Services.io.newURI(TEST_URL));
+ Assert.equal(tags[0], "testTag", "Tag on node has not changed");
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["testTag"]);
+ await PlacesUtils.bookmarks.remove(this._bookmark);
+ let bm = await PlacesUtils.bookmarks.fetch(this._bookmark.guid);
+ Assert.ok(!bm, "should have been removed");
+ },
+});
+
+// ------------------------------------------------------------------------------
+// Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog
+
+gTests.push({
+ desc: `Bug 491269 - Test that editing folder name in bookmarks properties dialog does not accept the dialog`,
+ sidebar: SIDEBAR_HISTORY_ID,
+ dialogUrl: DIALOG_URL,
+ action: ACTION_ADD,
+ historyView: SIDEBAR_HISTORY_BYLASTVISITED_VIEW,
+ window: null,
+
+ async setup() {
+ // Add a visit.
+ await PlacesTestUtils.addVisits(TEST_URL);
+ },
+
+ selectNode(tree) {
+ var visitNode = tree.view.nodeForTreeIndex(0);
+ tree.selectNode(visitNode);
+ Assert.equal(
+ tree.selectedNode.uri,
+ TEST_URL,
+ "The correct visit has been selected"
+ );
+ Assert.equal(
+ tree.selectedNode.itemId,
+ -1,
+ "The selected node is not bookmarked"
+ );
+ },
+
+ async run() {
+ // Open folder selector.
+ var foldersExpander = this.window.document.getElementById(
+ "editBMPanel_foldersExpander"
+ );
+ var folderTree = this.window.gEditItemOverlay._folderTree;
+ var self = this;
+
+ let unloadPromise = new Promise(resolve => {
+ this.window.addEventListener(
+ "unload",
+ event => {
+ Assert.ok(
+ self._cleanShutdown,
+ "Dialog window should not be closed by pressing ESC in folder name textbox"
+ );
+ executeSoon(() => {
+ resolve();
+ });
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ const observer = new this.window.MutationObserver(
+ (aMutationList, aObserver) => {
+ for (const mutation of aMutationList) {
+ if (
+ mutation.type != "attributes" ||
+ mutation.attributeName != "place"
+ ) {
+ continue;
+ }
+ aObserver.disconnect();
+ executeSoon(async function () {
+ // Create a new folder.
+ var newFolderButton = self.window.document.getElementById(
+ "editBMPanel_newFolderButton"
+ );
+ newFolderButton.doCommand();
+
+ // Wait for the folder to be created and for editing to start.
+ await TestUtils.waitForCondition(
+ () => folderTree.hasAttribute("editing"),
+ "We are editing new folder name in folder tree"
+ );
+
+ // Press Escape to discard editing new folder name.
+ EventUtils.synthesizeKey("VK_ESCAPE", {}, self.window);
+ Assert.ok(
+ !folderTree.hasAttribute("editing"),
+ "We have finished editing folder name in folder tree"
+ );
+
+ self._cleanShutdown = true;
+
+ self.window.document
+ .getElementById("bookmarkpropertiesdialog")
+ .cancelDialog();
+ });
+ break;
+ }
+ }
+ );
+ observer.observe(folderTree, { attributes: true });
+ foldersExpander.doCommand();
+ await unloadPromise;
+ },
+
+ finish() {
+ SidebarUI.hide();
+ },
+
+ async cleanup() {
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ await PlacesUtils.history.clear();
+ },
+});
+
+// ------------------------------------------------------------------------------
+
+add_task(async function test_setup() {
+ // This test can take some time, if we timeout too early it could run
+ // in the middle of other tests, or hang them.
+ requestLongerTimeout(2);
+});
+
+add_task(async function test_run() {
+ for (let test of gTests) {
+ info(`Start of test: ${test.desc}`);
+ await test.setup();
+
+ await execute_test_in_sidebar(test);
+ await test.run();
+
+ await test.cleanup();
+ await test.finish();
+
+ info(`End of test: ${test.desc}`);
+ }
+});
+
+/**
+ * Global functions to run a test in Properties dialog context.
+ */
+
+function execute_test_in_sidebar(test) {
+ return new Promise(resolve => {
+ var sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener(
+ "load",
+ function () {
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ executeSoon(async () => {
+ await open_properties_dialog(test);
+ resolve();
+ });
+ },
+ { capture: true, once: true }
+ );
+ SidebarUI.show(test.sidebar);
+ });
+}
+
+async function promise_properties_window(dialogUrl = DIALOG_URL) {
+ let win = await BrowserTestUtils.promiseAlertDialogOpen(null, dialogUrl, {
+ isSubDialog: true,
+ });
+ await SimpleTest.promiseFocus(win);
+ await win.document.mozSubdialogReady;
+ return win;
+}
+
+async function open_properties_dialog(test) {
+ var sidebar = document.getElementById("sidebar");
+
+ // If this is history sidebar, set the required view.
+ if (test.sidebar == SIDEBAR_HISTORY_ID) {
+ sidebar.contentDocument.getElementById(test.historyView).doCommand();
+ }
+
+ // Get sidebar's Places tree.
+ var sidebarTreeID =
+ test.sidebar == SIDEBAR_BOOKMARKS_ID
+ ? SIDEBAR_BOOKMARKS_TREE_ID
+ : SIDEBAR_HISTORY_TREE_ID;
+ var tree = sidebar.contentDocument.getElementById(sidebarTreeID);
+ // The sidebar may take a moment to open from the doCommand, therefore wait
+ // until it has opened before continuing.
+ await TestUtils.waitForCondition(() => tree, "Sidebar tree has been loaded");
+
+ // Ask current test to select the node to edit.
+ test.selectNode(tree);
+ Assert.ok(
+ tree.selectedNode,
+ "We have a places node selected: " + tree.selectedNode.title
+ );
+
+ return new Promise(resolve => {
+ var command = null;
+ switch (test.action) {
+ case ACTION_EDIT:
+ command = "placesCmd_show:info";
+ break;
+ case ACTION_ADD:
+ if (test.sidebar == SIDEBAR_BOOKMARKS_ID) {
+ if (test.itemType == TYPE_FOLDER) {
+ command = "placesCmd_new:folder";
+ } else if (test.itemType == TYPE_BOOKMARK) {
+ command = "placesCmd_new:bookmark";
+ } else {
+ Assert.ok(
+ false,
+ "You didn't set a valid itemType for adding an item"
+ );
+ }
+ } else {
+ command = "placesCmd_createBookmark";
+ }
+ break;
+ default:
+ Assert.ok(false, "You didn't set a valid action for this test");
+ }
+ // Ensure command is enabled for this node.
+ Assert.ok(
+ tree.controller.isCommandEnabled(command),
+ " command '" + command + "' on current selected node is enabled"
+ );
+
+ promise_properties_window(test.dialogUrl).then(win => {
+ test.window = win;
+ resolve();
+ });
+ // This will open the dialog. For some reason this needs to be executed
+ // later, as otherwise opening the dialog throws an exception.
+ executeSoon(() => {
+ tree.controller.doCommand(command);
+ });
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarks_change_title.js b/browser/components/places/tests/browser/browser_bookmarks_change_title.js
new file mode 100644
index 0000000000..0e24df188a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarks_change_title.js
@@ -0,0 +1,256 @@
+/**
+ * Tests that the title of a bookmark can be changed from the bookmark star, toolbar, sidebar, and library.
+ */
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+const titleAfterFirstUpdate = "BookmarkStar title";
+
+function getToolbarNodeForItemGuid(aItemGuid) {
+ var children = document.getElementById("PlacesToolbarItems").children;
+ for (let child of children) {
+ if (aItemGuid == child._placesNode.bookmarkGuid) {
+ return child;
+ }
+ }
+ return null;
+}
+
+// Setup.
+add_setup(async function () {
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ registerCleanupFunction(async () => {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_change_title_from_BookmarkStar() {
+ let originalBm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: "Before Edit",
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ win.StarUI._createPanelIfNeeded();
+ let bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+ let shownPromise = promisePopupShown(bookmarkPanel);
+
+ let bookmarkStar = win.BookmarkingUI.star;
+ bookmarkStar.click();
+
+ await shownPromise;
+
+ let bookmarkPanelTitle = win.document.getElementById(
+ "editBookmarkPanelTitle"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ bookmarkPanelTitle.textContent ===
+ gFluentStrings.formatValueSync("bookmarks-edit-bookmark"),
+ "Wait until the bookmark panel title will be changed expectedly."
+ );
+ Assert.ok(true, "Bookmark title is correct");
+
+ let promiseNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-title-changed",
+ events => events.some(e => e.title === titleAfterFirstUpdate)
+ );
+
+ // Update the bookmark's title.
+ await fillBookmarkTextField(
+ "editBMPanel_namePicker",
+ titleAfterFirstUpdate,
+ win
+ );
+
+ let doneButton = win.document.getElementById("editBookmarkPanelDoneButton");
+ doneButton.click();
+ await promiseNotification;
+
+ let updatedBm = await PlacesUtils.bookmarks.fetch(originalBm.guid);
+ Assert.equal(
+ updatedBm.title,
+ titleAfterFirstUpdate,
+ "Should have updated the bookmark title in the database"
+ );
+ await PlacesUtils.bookmarks.remove(originalBm.guid);
+});
+
+add_task(async function test_change_title_from_Toolbar() {
+ let toolbarBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: titleAfterFirstUpdate,
+ url: TEST_URL,
+ });
+
+ let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid);
+
+ await withBookmarksDialog(
+ false,
+ async function openPropertiesDialog() {
+ let placesContext = document.getElementById("placesContext");
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await promisePopup;
+
+ let properties = document.getElementById(
+ "placesContext_show_bookmark:info"
+ );
+ placesContext.activateItem(properties, {});
+ },
+ async function test(dialogWin) {
+ // Ensure the dialog has initialized.
+ await TestUtils.waitForCondition(() => dialogWin.document.title);
+
+ let namepicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+
+ let editBookmarkDialogTitle =
+ dialogWin.document.getElementById("titleText");
+ let bundle = dialogWin.document.getElementById("stringBundle");
+
+ Assert.equal(
+ bundle.getString("dialogTitleEditBookmark2"),
+ editBookmarkDialogTitle.textContent
+ );
+
+ Assert.equal(
+ namepicker.value,
+ titleAfterFirstUpdate,
+ "Name field is correct before update."
+ );
+
+ let promiseTitleChange = PlacesTestUtils.waitForNotification(
+ "bookmark-title-changed",
+ events => events.some(e => e.title === "Toolbar title")
+ );
+
+ // Update the bookmark's title.
+ fillBookmarkTextField(
+ "editBMPanel_namePicker",
+ "Toolbar title",
+ dialogWin,
+ false
+ );
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ await promiseTitleChange;
+
+ let updatedBm = await PlacesUtils.bookmarks.fetch(toolbarBookmark.guid);
+ Assert.equal(
+ updatedBm.title,
+ "Toolbar title",
+ "Should have updated the bookmark title in the database"
+ );
+ }
+ );
+});
+
+add_task(async function test_change_title_from_Sidebar() {
+ let bookmarks = [];
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm =>
+ bookmarks.push(bm)
+ );
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ tree.selectItems([bookmarks[0].guid]);
+
+ await withBookmarksDialog(
+ false,
+ function openPropertiesDialog() {
+ tree.controller.doCommand("placesCmd_show:info");
+ },
+ async function test(dialogWin) {
+ let namepicker = dialogWin.document.getElementById(
+ "editBMPanel_namePicker"
+ );
+ Assert.equal(
+ namepicker.value,
+ "Toolbar title",
+ "Name field is correct before update."
+ );
+
+ let promiseTitleChange = PlacesTestUtils.waitForNotification(
+ "bookmark-title-changed",
+ events => events.some(e => e.title === "Sidebar Title")
+ );
+
+ // Update the bookmark's title.
+ fillBookmarkTextField(
+ "editBMPanel_namePicker",
+ "Sidebar Title",
+ dialogWin,
+ false
+ );
+
+ // Confirm and close the dialog.
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ await promiseTitleChange;
+
+ // Get updated bookmarks, check the new title.
+ bookmarks = [];
+ await PlacesUtils.bookmarks.fetch({ url: TEST_URL }, bm =>
+ bookmarks.push(bm)
+ );
+ Assert.equal(
+ bookmarks[0].title,
+ "Sidebar Title",
+ "Should have updated the bookmark title in the database"
+ );
+ Assert.equal(bookmarks.length, 1, "One bookmark should exist");
+ }
+ );
+ });
+});
+
+add_task(async function test_change_title_from_Library() {
+ info("Open library and select the bookmark.");
+ const library = await promiseLibrary("BookmarksToolbar");
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ });
+ library.ContentTree.view.selectNode(
+ library.ContentTree.view.view.nodeForTreeIndex(0)
+ );
+ const newTitle = "Library";
+ const promiseTitleChange = PlacesTestUtils.waitForNotification(
+ "bookmark-title-changed",
+ events => events.some(e => e.title === newTitle)
+ );
+ info("Update the bookmark's title.");
+ fillBookmarkTextField("editBMPanel_namePicker", newTitle, library);
+ await promiseTitleChange;
+ info("The bookmark's title was updated.");
+ await promiseLibraryClosed(library);
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarks_change_url.js b/browser/components/places/tests/browser/browser_bookmarks_change_url.js
new file mode 100644
index 0000000000..ab5ad742d4
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarks_change_url.js
@@ -0,0 +1,106 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/*
+ * Test whether or not that url field in library window will update properly
+ * when changing it.
+ */
+
+const TEST_URLS = ["https://example.com/", "https://example.org/"];
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function basic() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URLS[0],
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URLS[1],
+ });
+
+ info("Open library for bookmarks toolbar");
+ const library = await promiseLibrary("BookmarksToolbar");
+
+ info("Check the initial content");
+ const tree = library.document.getElementById("placeContent");
+ Assert.equal(tree.view.rowCount, 2);
+ assertRow(tree, 0, TEST_URLS[0]);
+ assertRow(tree, 1, TEST_URLS[1]);
+
+ info("Check the url");
+ const newURL = `${TEST_URLS[0]}/?test`;
+ await updateURL(newURL, library);
+
+ info("Check the update");
+ Assert.equal(tree.view.rowCount, 2);
+ assertRow(tree, 0, newURL);
+ assertRow(tree, 1, TEST_URLS[1]);
+
+ info("Close library window");
+ await promiseLibraryClosed(library);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function whileFiltering() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URLS[0],
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URLS[1],
+ });
+
+ info("Open library for bookmarks toolbar");
+ const library = await promiseLibrary("BookmarksToolbar");
+
+ info("Check the initial content");
+ const tree = library.document.getElementById("placeContent");
+ Assert.equal(tree.view.rowCount, 2);
+ assertRow(tree, 0, TEST_URLS[0]);
+ assertRow(tree, 1, TEST_URLS[1]);
+
+ info("Filter by search chars");
+ library.PlacesSearchBox.search("org");
+ Assert.equal(tree.view.rowCount, 1);
+ assertRow(tree, 0, TEST_URLS[1]);
+
+ info("Check the url");
+ const newURL = `${TEST_URLS[1]}/?test`;
+ await updateURL(newURL, library);
+
+ info("Check the update");
+ Assert.equal(tree.view.rowCount, 1);
+ assertRow(tree, 0, newURL);
+
+ info("Close library window");
+ await promiseLibraryClosed(library);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+async function updateURL(newURL, library) {
+ const promiseUrlChange = PlacesTestUtils.waitForNotification(
+ "bookmark-url-changed",
+ () => true
+ );
+ fillBookmarkTextField("editBMPanel_locationField", newURL, library);
+ await promiseUrlChange;
+}
+
+function assertRow(tree, targeRow, expectedUrl) {
+ const url = tree.view.getCellText(targeRow, tree.columns.placesContentUrl);
+ Assert.equal(url, expectedUrl, "URL is correct");
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js b/browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js
new file mode 100644
index 0000000000..a425a9b031
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js
@@ -0,0 +1,213 @@
+/**
+ * Test searching for bookmarks (by title and by tag) from the Bookmarks sidebar.
+ */
+"use strict";
+
+let sidebar = document.getElementById("sidebar");
+
+const TEST_URI = "http://example.com/";
+const BOOKMARKS_COUNT = 4;
+const TEST_PARENT_FOLDER = "testParentFolder";
+const TEST_SIF_URL = "http://testsif.example.com/";
+const TEST_SIF_TITLE = "TestSIF";
+const TEST_NEW_TITLE = "NewTestSIF";
+
+function assertBookmarks(searchValue) {
+ let found = 0;
+
+ let searchBox = sidebar.contentDocument.getElementById("search-box");
+
+ ok(searchBox, "search box is in context");
+
+ searchBox.value = searchValue;
+ searchBox.doCommand();
+
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+
+ for (let i = 0; i < tree.view.rowCount; i++) {
+ let cellText = tree.view.getCellText(i, tree.columns.getColumnAt(0));
+
+ if (cellText.includes("example page")) {
+ found++;
+ }
+ }
+
+ info("Reset the search");
+ searchBox.value = "";
+ searchBox.doCommand();
+
+ is(found, BOOKMARKS_COUNT, "found expected site");
+}
+
+async function showInFolder(aSearchStr, aParentFolderGuid) {
+ let searchBox = sidebar.contentDocument.getElementById("search-box");
+
+ searchBox.value = aSearchStr;
+ searchBox.doCommand();
+
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ let theNode = tree.view._getNodeForRow(0);
+ let bookmarkGuid = theNode.bookmarkGuid;
+
+ Assert.equal(theNode.uri, TEST_SIF_URL, "Found expected bookmark");
+
+ info("Running Show in Folder command");
+ tree.selectNode(theNode);
+ tree.controller.doCommand("placesCmd_showInFolder");
+
+ let treeNode = tree.selectedNode;
+ Assert.equal(
+ treeNode.parent.bookmarkGuid,
+ aParentFolderGuid,
+ "Containing folder node is correct"
+ );
+ Assert.equal(
+ treeNode.bookmarkGuid,
+ bookmarkGuid,
+ "The searched bookmark guid matches selected node"
+ );
+ Assert.equal(
+ treeNode.uri,
+ TEST_SIF_URL,
+ "The searched bookmark URL matches selected node"
+ );
+
+ info("Check the title will be applied the item when changing it");
+ await PlacesUtils.bookmarks.update({
+ guid: theNode.bookmarkGuid,
+ title: TEST_NEW_TITLE,
+ });
+ Assert.equal(
+ treeNode.title,
+ TEST_NEW_TITLE,
+ "New title is applied to the node"
+ );
+}
+
+add_task(async function testTree() {
+ // Add bookmarks and tags.
+ for (let i = 0; i < BOOKMARKS_COUNT; i++) {
+ let url = Services.io.newURI(TEST_URI + i);
+
+ await PlacesUtils.bookmarks.insert({
+ url,
+ title: "example page " + i,
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ PlacesUtils.tagging.tagURI(url, ["test"]);
+ }
+
+ await withSidebarTree("bookmarks", function () {
+ // Search a bookmark by its title.
+ assertBookmarks("example.com");
+ // Search a bookmark by its tag.
+ assertBookmarks("test");
+ });
+
+ // Cleanup before testing Show in Folder.
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function testShowInFolder() {
+ // Now test Show in Folder
+ let parentFolder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: TEST_PARENT_FOLDER,
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: parentFolder.guid,
+ title: TEST_SIF_TITLE,
+ url: TEST_SIF_URL,
+ });
+
+ await withSidebarTree("bookmarks", async function () {
+ await showInFolder(TEST_SIF_TITLE, parentFolder.guid);
+ });
+
+ // Cleanup
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function testRenameOnQueryResult() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: TEST_SIF_TITLE,
+ url: TEST_SIF_URL,
+ });
+
+ await withSidebarTree("bookmarks", async function () {
+ const searchBox = sidebar.contentDocument.getElementById("search-box");
+
+ searchBox.value = TEST_SIF_TITLE;
+ searchBox.doCommand();
+
+ const tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ const theNode = tree.view._getNodeForRow(0);
+
+ info("Check the found bookmark");
+ Assert.equal(theNode.uri, TEST_SIF_URL, "URI of bookmark found is correct");
+ Assert.equal(
+ theNode.title,
+ TEST_SIF_TITLE,
+ "Title of bookmark found is correct"
+ );
+
+ info("Check the title will be applied the item when changing it");
+ await PlacesUtils.bookmarks.update({
+ guid: theNode.bookmarkGuid,
+ title: TEST_NEW_TITLE,
+ });
+
+ // As the query result is refreshed once then the node also is regenerated,
+ // need to get the result node from the tree again.
+ Assert.equal(
+ tree.view._getNodeForRow(0).bookmarkGuid,
+ theNode.bookmarkGuid,
+ "GUID of node regenerated is correct"
+ );
+ Assert.equal(
+ tree.view._getNodeForRow(0).uri,
+ theNode.uri,
+ "URI of node regenerated is correct"
+ );
+ Assert.equal(
+ tree.view._getNodeForRow(0).parentGuid,
+ theNode.parentGuid,
+ "parentGuid of node regenerated is correct"
+ );
+ Assert.equal(
+ tree.view._getNodeForRow(0).title,
+ TEST_NEW_TITLE,
+ "New title is applied to the node"
+ );
+
+ info("Check the new date will be applied the item when changing it");
+ const now = new Date();
+ await PlacesUtils.bookmarks.update({
+ guid: theNode.bookmarkGuid,
+ dateAdded: now,
+ lastModified: now,
+ });
+
+ Assert.equal(
+ tree.view._getNodeForRow(0).uri,
+ theNode.uri,
+ "URI of node regenerated is correct"
+ );
+ Assert.equal(
+ tree.view._getNodeForRow(0).dateAdded,
+ now.getTime() * 1000,
+ "New dateAdded is applied to the node"
+ );
+ Assert.equal(
+ tree.view._getNodeForRow(0).lastModified,
+ now.getTime() * 1000,
+ "New lastModified is applied to the node"
+ );
+ });
+
+ // Cleanup before testing Show in Folder.
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js b/browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js
new file mode 100644
index 0000000000..6e1f5e93c2
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_task(async function testPopup() {
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://example.com",
+ title: "firefox",
+ });
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.remove(bookmark);
+ Services.prefs.clearUserPref("browser.toolbars.bookmarks.visibility");
+ });
+
+ for (let state of ["always", "newtab"]) {
+ info(`Testing with state set to '${state}'`);
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", state]],
+ });
+
+ let newtab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: "about:newtab",
+ waitForLoad: false,
+ });
+
+ let bookmarksToolbar = document.getElementById("PersonalToolbar");
+ await TestUtils.waitForCondition(
+ () => !bookmarksToolbar.collapsed,
+ "Wait for toolbar to become visible"
+ );
+ ok(!bookmarksToolbar.collapsed, "Bookmarks toolbar should be visible");
+
+ // 1. Right-click on a bookmark and check that the submenu is visible
+ let bookmarkItem = bookmarksToolbar.querySelector(
+ `.bookmark-item[label="firefox"]`
+ );
+ ok(bookmarkItem, "Got bookmark");
+ let contextMenu = document.getElementById("placesContext");
+ let popup = await openContextMenu(contextMenu, bookmarkItem);
+ ok(
+ !popup.target.querySelector("#toggle_PersonalToolbar").hidden,
+ "Bookmarks toolbar submenu should appear on a .bookmark-item"
+ );
+ contextMenu.hidePopup();
+
+ // 2. Right-click on the empty area and check that the submenu is visible
+ popup = await openContextMenu(contextMenu, bookmarksToolbar);
+ ok(
+ !popup.target.querySelector("#toggle_PersonalToolbar").hidden,
+ "Bookmarks toolbar submenu should appear on the empty part of the toolbar"
+ );
+
+ let bookmarksToolbarMenu = document.querySelector(
+ "#toggle_PersonalToolbar"
+ );
+ let subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ bookmarksToolbarMenu.openMenu(true);
+ await BrowserTestUtils.waitForPopupEvent(subMenu, "shown");
+ let menuitems = subMenu.querySelectorAll("menuitem");
+ for (let menuitem of menuitems) {
+ let expected = menuitem.dataset.visibilityEnum == state;
+ is(
+ menuitem.getAttribute("checked"),
+ expected.toString(),
+ `The corresponding menuitem, ${menuitem.dataset.visibilityEnum}, ${
+ expected ? "should" : "shouldn't"
+ } be checked if state=${state}`
+ );
+ }
+
+ contextMenu.hidePopup();
+
+ BrowserTestUtils.removeTab(newtab);
+ }
+});
+
+function openContextMenu(contextMenu, target) {
+ let popupPromise = BrowserTestUtils.waitForPopupEvent(contextMenu, "shown");
+ EventUtils.synthesizeMouseAtCenter(target, {
+ type: "contextmenu",
+ button: 2,
+ });
+ return popupPromise;
+}
diff --git a/browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js b/browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js
new file mode 100644
index 0000000000..241710da3b
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js
@@ -0,0 +1,162 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const SCALAR_NAME = "browser.ui.customized_widgets";
+const bookmarksInfo = [
+ {
+ title: "firefox",
+ url: "http://example.com",
+ },
+ {
+ title: "rules",
+ url: "http://example.com/2",
+ },
+ {
+ title: "yo",
+ url: "http://example.com/2",
+ },
+];
+
+// Setup.
+add_task(async function test_bookmarks_toolbar_telemetry() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.toolbars.bookmarks.visibility", "newtab"]],
+ });
+
+ // This is added during startup
+ await TestUtils.waitForCondition(
+ () =>
+ keyedScalarExists(
+ "browser.ui.toolbar_widgets",
+ "bookmarks-bar_pinned_newtab",
+ true
+ ),
+ `Waiting for "bookmarks-bar_pinned_newtab" to appear in Telemetry snapshot`
+ );
+ ok(true, `"bookmarks-bar_pinned_newtab"=true found in Telemetry`);
+
+ await changeToolbarVisibilityViaContextMenu("never");
+ await assertUIChange(
+ "bookmarks-bar_move_newtab_never_toolbar-context-menu",
+ 1
+ );
+
+ await changeToolbarVisibilityViaContextMenu("newtab");
+ await assertUIChange(
+ "bookmarks-bar_move_never_newtab_toolbar-context-menu",
+ 1
+ );
+
+ await changeToolbarVisibilityViaContextMenu("always");
+ await assertUIChange(
+ "bookmarks-bar_move_newtab_always_toolbar-context-menu",
+ 1
+ );
+
+ // Extra windows are opened to make sure telemetry numbers aren't
+ // double counted since there will be multiple instances of the
+ // bookmarks toolbar.
+ let extraWindows = [];
+ extraWindows.push(await BrowserTestUtils.openNewBrowserWindow());
+
+ Services.telemetry.getSnapshotForScalars("main", true);
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarksInfo,
+ });
+ registerCleanupFunction(() => PlacesUtils.bookmarks.remove(bookmarks));
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "browser.engagement.bookmarks_toolbar_bookmark_added",
+ 3,
+ "Bookmarks added value should be 3"
+ );
+
+ let newtab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(() => BrowserTestUtils.removeTab(newtab));
+ let bookmarkToolbarButton = document.querySelector(
+ "#PlacesToolbarItems > toolbarbutton"
+ );
+ bookmarkToolbarButton.click();
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "browser.engagement.bookmarks_toolbar_bookmark_opened",
+ 1,
+ "Bookmarks opened value should be 1"
+ );
+
+ extraWindows.push(await BrowserTestUtils.openNewBrowserWindow());
+ extraWindows.push(await BrowserTestUtils.openNewBrowserWindow());
+
+ // Simulate dragging a bookmark within the toolbar to ensure
+ // that the bookmarks_toolbar_bookmark_added probe doesn't increment
+ let srcElement = document.querySelector("#PlacesToolbar .bookmark-item");
+ let destElement = document.querySelector("#PlacesToolbar");
+ await EventUtils.synthesizePlainDragAndDrop({
+ srcElement,
+ destElement,
+ });
+
+ TelemetryTestUtils.assertScalar(
+ TelemetryTestUtils.getProcessScalars("parent"),
+ "browser.engagement.bookmarks_toolbar_bookmark_added",
+ 3,
+ "Bookmarks added value should still be 3"
+ );
+
+ for (let win of extraWindows) {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
+
+async function changeToolbarVisibilityViaContextMenu(nextState) {
+ let contextMenu = document.querySelector("#toolbar-context-menu");
+ let popupShown = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ let menuButton = document.getElementById("PanelUI-menu-button");
+ EventUtils.synthesizeMouseAtCenter(
+ menuButton,
+ { type: "contextmenu" },
+ window
+ );
+ await popupShown;
+ let bookmarksToolbarMenu = document.querySelector("#toggle_PersonalToolbar");
+ let subMenu = bookmarksToolbarMenu.querySelector("menupopup");
+ popupShown = BrowserTestUtils.waitForEvent(subMenu, "popupshown");
+ bookmarksToolbarMenu.openMenu(true);
+ await popupShown;
+ let menuItem = document.querySelector(
+ `menuitem[data-visibility-enum="${nextState}"]`
+ );
+ subMenu.activateItem(menuItem);
+ contextMenu.hidePopup();
+}
+
+async function assertUIChange(key, value) {
+ await TestUtils.waitForCondition(
+ () => keyedScalarExists(SCALAR_NAME, key, value),
+ `Waiting for ${key} to appear in Telemetry snapshot`
+ );
+ ok(true, `${key}=${value} found in Telemetry`);
+}
+
+function keyedScalarExists(scalar, key, value) {
+ let snapshot = Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).parent;
+ if (!snapshot.hasOwnProperty(scalar)) {
+ return false;
+ }
+ if (!snapshot[scalar].hasOwnProperty(key)) {
+ info(`Looking for ${key} in ${JSON.stringify(snapshot[scalar])}`);
+ return false;
+ }
+ return snapshot[scalar][key] == value;
+}
diff --git a/browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js b/browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js
new file mode 100644
index 0000000000..6acb6fb04c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js
@@ -0,0 +1,50 @@
+"use strict";
+
+/**
+ * Bug 427633 - Disable creating a New Folder in the bookmarks dialogs if
+ * insertionPoint is invalid.
+ */
+
+const TEST_URL = "about:buildconfig";
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ registerCleanupFunction(async () => {
+ bookmarkPanel.removeAttribute("animate");
+ await BrowserTestUtils.removeTab(tab);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ StarUI._createPanelIfNeeded();
+ let bookmarkPanel = document.getElementById("editBookmarkPanel");
+ bookmarkPanel.setAttribute("animate", false);
+
+ let shownPromise = promisePopupShown(bookmarkPanel);
+
+ let bookmarkStar = BookmarkingUI.star;
+ bookmarkStar.click();
+
+ await shownPromise;
+
+ ok(gEditItemOverlay, "gEditItemOverlay is in context");
+ ok(gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+
+ window.gEditItemOverlay.toggleFolderTreeVisibility();
+
+ let tree = gEditItemOverlay._element("folderTree");
+
+ tree.view.selection.clearSelection();
+ ok(
+ document.getElementById("editBMPanel_newFolderButton").disabled,
+ "New folder button is disabled if there's no selection"
+ );
+
+ let hiddenPromise = promisePopupHidden(bookmarkPanel);
+ document.getElementById("editBookmarkPanelRemoveButton").click();
+ await hiddenPromise;
+});
diff --git a/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js b/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js
new file mode 100644
index 0000000000..9abb82c9bb
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js
@@ -0,0 +1,78 @@
+"use strict";
+
+const TEST_URL = "https://www.example.com/";
+const testTag = "foo";
+const testTagUpper = "Foo";
+const testURI = Services.io.newURI(TEST_URL);
+
+add_task(async function test() {
+ // Add a bookmark.
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "mozilla",
+ url: testURI,
+ });
+
+ PlacesUtils.tagging.tagURI(makeURI(TEST_URL), ["tag0"]);
+
+ const win = await BrowserTestUtils.openNewBrowserWindow();
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ });
+ win.StarUI._createPanelIfNeeded();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser: win.gBrowser,
+ url: TEST_URL,
+ },
+ async () => {
+ // Init panel
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status !== BookmarkingUI.STATUS_UPDATING
+ );
+
+ await clickBookmarkStar(win);
+
+ // add a tag
+ await fillBookmarkTextField("editBMPanel_tagsField", testTag, win);
+
+ let promiseNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-tags-changed"
+ );
+ await hideBookmarksPanel(win);
+ await promiseNotification;
+
+ // test that the tag has been added in the backend
+ is(PlacesUtils.tagging.getTagsForURI(testURI)[0], testTag, "tags match");
+
+ // change the tag
+ await TestUtils.waitForCondition(
+ () => BookmarkingUI.status !== BookmarkingUI.STATUS_UPDATING
+ );
+ await clickBookmarkStar(win);
+ await fillBookmarkTextField("editBMPanel_tagsField", testTagUpper, win);
+ // The old sync API doesn't notify a tags change, and fixing it would be
+ // quite complex, so we just wait for a title change until tags are
+ // refactored.
+ promiseNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-title-changed"
+ );
+ await hideBookmarksPanel(win);
+ await promiseNotification;
+
+ // test that the tag has been added in the backend
+ is(
+ PlacesUtils.tagging.getTagsForURI(testURI)[0],
+ testTagUpper,
+ "tags match"
+ );
+
+ // Cleanup.
+ PlacesUtils.tagging.untagURI(testURI, [testTag]);
+ await PlacesUtils.bookmarks.remove(bm.guid);
+ }
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js b/browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js
new file mode 100644
index 0000000000..5d35356a69
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js
@@ -0,0 +1,162 @@
+/**
+ * This test checks that editing tags doesn't scroll the tags selector
+ * listbox to wrong positions.
+ */
+
+const TEST_URL = "about:buildconfig";
+
+function scrolledIntoView(item, parentItem) {
+ let itemRect = item.getBoundingClientRect();
+ let parentItemRect = parentItem.getBoundingClientRect();
+ let pointInView = y => parentItemRect.top < y && y < parentItemRect.bottom;
+
+ // Partially visible items are also considered visible.
+ return pointInView(itemRect.top) || pointInView(itemRect.bottom);
+}
+
+add_task(async function runTest() {
+ await PlacesUtils.bookmarks.eraseEverything();
+ let tags = [
+ "a",
+ "b",
+ "c",
+ "d",
+ "e",
+ "f",
+ "g",
+ "h",
+ "i",
+ "l",
+ "m",
+ "n",
+ "o",
+ "p",
+ ];
+
+ // Add a bookmark and tag it.
+ let uri1 = Services.io.newURI(TEST_URL);
+ let bm1 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "mozilla",
+ url: uri1.spec,
+ });
+ PlacesUtils.tagging.tagURI(uri1, tags);
+
+ // Add a second bookmark so that tags won't disappear when unchecked.
+ let uri2 = Services.io.newURI("http://www2.mozilla.org/");
+ let bm2 = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "mozilla",
+ url: uri2.spec,
+ });
+ PlacesUtils.tagging.tagURI(uri2, tags);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ registerCleanupFunction(async () => {
+ bookmarkPanel.removeAttribute("animate");
+ await BrowserTestUtils.closeWindow(win);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ win.StarUI._createPanelIfNeeded();
+ let bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+ bookmarkPanel.setAttribute("animate", false);
+ let shownPromise = promisePopupShown(bookmarkPanel);
+
+ let bookmarkStar = win.BookmarkingUI.star;
+ bookmarkStar.click();
+
+ await shownPromise;
+
+ // Init panel.
+ ok(win.gEditItemOverlay, "gEditItemOverlay is in context");
+ ok(win.gEditItemOverlay.initialized, "gEditItemOverlay is initialized");
+
+ await openTagSelector(win);
+ let tagsSelector = win.document.getElementById("editBMPanel_tagsSelector");
+
+ // Go by two so there is some untouched tag in the middle.
+ for (let i = 8; i < tags.length; i += 2) {
+ tagsSelector.selectedIndex = i;
+ let listItem = tagsSelector.selectedItem;
+ isnot(listItem, null, "Valid listItem found");
+
+ tagsSelector.ensureElementIsVisible(listItem);
+ let scrollTop = tagsSelector.scrollTop;
+
+ ok(listItem.hasAttribute("checked"), "Item is checked " + i);
+ let selectedTag = listItem.label;
+
+ // Uncheck the tag.
+ let promise = BrowserTestUtils.waitForEvent(
+ tagsSelector,
+ "BookmarkTagsSelectorUpdated"
+ );
+ EventUtils.synthesizeMouseAtCenter(listItem.firstElementChild, {}, win);
+ await promise;
+ is(scrollTop, tagsSelector.scrollTop, "Scroll position did not change");
+
+ // The listbox is rebuilt, so we have to get the new element.
+ let newItem = tagsSelector.selectedItem;
+ isnot(newItem, null, "Valid new listItem found");
+ ok(!newItem.hasAttribute("checked"), "New listItem is unchecked " + i);
+ is(newItem.label, selectedTag, "Correct tag is still selected");
+
+ // Check the tag.
+ promise = BrowserTestUtils.waitForEvent(
+ tagsSelector,
+ "BookmarkTagsSelectorUpdated"
+ );
+ EventUtils.synthesizeMouseAtCenter(newItem.firstElementChild, {}, win);
+ await promise;
+ is(scrollTop, tagsSelector.scrollTop, "Scroll position did not change");
+ }
+
+ // Remove the second bookmark, then nuke some of the tags.
+ await PlacesUtils.bookmarks.remove(bm2);
+
+ // Allow the tag updates to complete
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ // Doing this backwords tests more interesting paths.
+ for (let i = tags.length - 1; i >= 0; i -= 2) {
+ tagsSelector.selectedIndex = i;
+ let listItem = tagsSelector.selectedItem;
+ isnot(listItem, null, "Valid listItem found");
+
+ tagsSelector.ensureElementIsVisible(listItem);
+
+ ok(listItem.hasAttribute("checked"), "Item is checked " + i);
+
+ // Uncheck the tag.
+ let promise = BrowserTestUtils.waitForEvent(
+ tagsSelector,
+ "BookmarkTagsSelectorUpdated"
+ );
+ EventUtils.synthesizeMouseAtCenter(listItem.firstElementChild, {}, win);
+ await promise;
+ }
+
+ let hiddenPromise = promisePopupHidden(bookmarkPanel);
+ let doneButton = win.document.getElementById("editBookmarkPanelDoneButton");
+ doneButton.click();
+ await hiddenPromise;
+ // Cleanup.
+ await PlacesUtils.bookmarks.remove(bm1);
+});
+
+function openTagSelector(win) {
+ let promise = BrowserTestUtils.waitForEvent(
+ win.document.getElementById("editBMPanel_tagsSelector"),
+ "BookmarkTagsSelectorUpdated"
+ );
+ // Open the tags selector.
+ win.document.getElementById("editBMPanel_tagsSelectorExpander").doCommand();
+ return promise;
+}
diff --git a/browser/components/places/tests/browser/browser_check_correct_controllers.js b/browser/components/places/tests/browser/browser_check_correct_controllers.js
new file mode 100644
index 0000000000..80095823e1
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_check_correct_controllers.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+add_setup(async () => {
+ // Ensure all bookmarks cleared before the test starts.
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test() {
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "Plain Bob",
+ url: "http://example.com",
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.remove(bookmark);
+ });
+
+ let sidebarBox = document.getElementById("sidebar-box");
+ is(sidebarBox.hidden, true, "The sidebar should be hidden");
+
+ // Uncollapse the personal toolbar if needed.
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ let sidebar = await promiseLoadedSidebar("viewBookmarksSidebar");
+ registerCleanupFunction(() => {
+ SidebarUI.hide();
+ });
+
+ // Focus the tree and check if its controller is returned.
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ tree.focus();
+
+ let controller = PlacesUIUtils.getControllerForCommand(
+ window,
+ "placesCmd_copy"
+ );
+ let treeController =
+ tree.controllers.getControllerForCommand("placesCmd_copy");
+ Assert.equal(controller, treeController, "tree controller was returned");
+
+ // Open the context menu for a toolbar item, and check if the toolbar's
+ // controller is returned.
+ let toolbarItems = document.getElementById("PlacesToolbarItems");
+ // Ensure the toolbar has displayed the bookmark. This might be async, so
+ // wait a little if necessary.
+ await TestUtils.waitForCondition(
+ () => toolbarItems.children.length == 1,
+ "Should have only one item on the toolbar"
+ );
+
+ let placesContext = document.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouse(
+ toolbarItems.children[0],
+ 4,
+ 4,
+ { type: "contextmenu", button: 2 },
+ window
+ );
+ await popupShownPromise;
+
+ controller = PlacesUIUtils.getControllerForCommand(window, "placesCmd_copy");
+ let toolbarController = document
+ .getElementById("PlacesToolbar")
+ .controllers.getControllerForCommand("placesCmd_copy");
+ Assert.equal(
+ controller,
+ toolbarController,
+ "the toolbar controller was returned"
+ );
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popuphidden"
+ );
+ placesContext.hidePopup();
+ await popupHiddenPromise;
+
+ // Now that the context menu is closed, try to get the tree controller again.
+ tree.focus();
+ controller = PlacesUIUtils.getControllerForCommand(window, "placesCmd_copy");
+ Assert.equal(controller, treeController, "tree controller was returned");
+
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+});
+
+function promiseLoadedSidebar(cmd) {
+ return new Promise(resolve => {
+ let sidebar = document.getElementById("sidebar");
+ sidebar.addEventListener(
+ "load",
+ function () {
+ executeSoon(() => resolve(sidebar));
+ },
+ { capture: true, once: true }
+ );
+
+ SidebarUI.show(cmd);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js b/browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js
new file mode 100644
index 0000000000..5ecf95d94e
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js
@@ -0,0 +1,234 @@
+/* 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 PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs";
+const EXAMPLE_PAGE = "http://example.com/";
+const TEST_PAGES = [
+ "about:mozilla",
+ "about:robots",
+ "javascript:window.location=%22" + EXAMPLE_PAGE + "%22",
+];
+
+var gBookmarkElements = [];
+
+function waitForBookmarkElements(expectedCount) {
+ let container = document.getElementById("PlacesToolbarItems");
+ if (container.childElementCount == expectedCount) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => {
+ info("Waiting for bookmarks");
+ let mut = new MutationObserver(mutations => {
+ info("Elements appeared");
+ if (container.childElementCount == expectedCount) {
+ resolve();
+ mut.disconnect();
+ }
+ });
+
+ mut.observe(container, { childList: true });
+ });
+}
+
+function getToolbarNodeForItemGuid(aItemGuid) {
+ var children = document.getElementById("PlacesToolbarItems").children;
+ for (let child of children) {
+ if (aItemGuid == child._placesNode.bookmarkGuid) {
+ return child;
+ }
+ }
+ return null;
+}
+
+function waitForLoad(browser, url) {
+ return BrowserTestUtils.browserLoaded(browser, false, url);
+}
+
+function waitForNewTab(url, inBackground) {
+ return BrowserTestUtils.waitForNewTab(gBrowser, url).then(tab => {
+ if (inBackground) {
+ Assert.notEqual(
+ tab,
+ gBrowser.selectedTab,
+ `The new tab is in the background`
+ );
+ } else {
+ Assert.equal(
+ tab,
+ gBrowser.selectedTab,
+ `The new tab is in the foreground`
+ );
+ }
+
+ BrowserTestUtils.removeTab(tab);
+ });
+}
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ let bookmarks = await Promise.all(
+ TEST_PAGES.map((url, index) => {
+ return PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: `Title ${index}`,
+ url,
+ });
+ })
+ );
+
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ await waitForBookmarkElements(TEST_PAGES.length);
+ for (let bookmark of bookmarks) {
+ let element = getToolbarNodeForItemGuid(bookmark.guid);
+ Assert.notEqual(element, null, "Found node on toolbar");
+
+ gBookmarkElements.push(element);
+ }
+
+ registerCleanupFunction(async () => {
+ gBookmarkElements = [];
+
+ await Promise.all(
+ bookmarks.map(bookmark => {
+ return PlacesUtils.bookmarks.remove(bookmark);
+ })
+ );
+
+ // Note: hiding the toolbar before removing the bookmarks triggers
+ // bug 1766284.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ });
+});
+
+add_task(async function click() {
+ let promise = waitForLoad(gBrowser.selectedBrowser, TEST_PAGES[0]);
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], {
+ button: 0,
+ });
+ await promise;
+
+ promise = waitForNewTab(TEST_PAGES[1], false);
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], {
+ button: 0,
+ accelKey: true,
+ });
+ await promise;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: "about:blank",
+ });
+ promise = waitForLoad(gBrowser.selectedBrowser, EXAMPLE_PAGE);
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[2], {});
+ await promise;
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function middleclick() {
+ let promise = waitForNewTab(TEST_PAGES[0], true);
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], {
+ button: 1,
+ shiftKey: true,
+ });
+ await promise;
+
+ promise = waitForNewTab(TEST_PAGES[1], false);
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], {
+ button: 1,
+ });
+ await promise;
+});
+
+add_task(async function clickWithPrefSet() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]],
+ });
+
+ let promise = waitForNewTab(TEST_PAGES[0], false);
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], {
+ button: 0,
+ });
+ await promise;
+
+ // With loadBookmarksInTabs, reuse current tab if blank
+ for (let button of [0, 1]) {
+ await BrowserTestUtils.withNewTab({ gBrowser }, async tab => {
+ promise = waitForLoad(gBrowser.selectedBrowser, TEST_PAGES[1]);
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], {
+ button,
+ });
+ await promise;
+ });
+ }
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function openInSameTabWithPrefSet() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]],
+ });
+
+ let placesContext = document.getElementById("placesContext");
+ let popupPromise = BrowserTestUtils.waitForEvent(placesContext, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], {
+ button: 2,
+ type: "contextmenu",
+ });
+ info("Waiting for context menu");
+ await popupPromise;
+
+ let openItem = document.getElementById("placesContext_open");
+ ok(BrowserTestUtils.isVisible(openItem), "Open item should be visible");
+
+ info("Waiting for page to load");
+ let promise = waitForLoad(gBrowser.selectedBrowser, TEST_PAGES[0]);
+ openItem.doCommand();
+ placesContext.hidePopup();
+ await promise;
+
+ await SpecialPowers.popPrefEnv();
+});
+
+// Open a tab, then quickly open the context menu to ensure that the command
+// enabled state of the menuitems is updated properly.
+add_task(async function quickContextMenu() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]],
+ });
+
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, TEST_PAGES[0]);
+
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[0], {
+ button: 0,
+ });
+ let newTab = await tabPromise;
+
+ let placesContext = document.getElementById("placesContext");
+ let promise = BrowserTestUtils.waitForEvent(placesContext, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(gBookmarkElements[1], {
+ button: 2,
+ type: "contextmenu",
+ });
+ await promise;
+
+ Assert.ok(
+ !document.getElementById("placesContext_open:newtab").disabled,
+ "Commands in context menu are enabled"
+ );
+
+ promise = BrowserTestUtils.waitForEvent(placesContext, "popuphidden");
+ placesContext.hidePopup();
+ await promise;
+ BrowserTestUtils.removeTab(newTab);
+
+ await SpecialPowers.popPrefEnv();
+});
diff --git a/browser/components/places/tests/browser/browser_controller_onDrop.js b/browser/components/places/tests/browser/browser_controller_onDrop.js
new file mode 100644
index 0000000000..cbda2612cf
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_controller_onDrop.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+var bookmarks;
+var bookmarkIds;
+var library;
+
+add_setup(async function () {
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await promiseLibraryClosed(library);
+ });
+
+ bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "bm1",
+ url: "http://example1.com",
+ },
+ {
+ title: "bm2",
+ url: "http://example2.com",
+ },
+ {
+ title: "bm3",
+ url: "http://example3.com",
+ },
+ ],
+ });
+
+ bookmarkIds = await PlacesTestUtils.promiseManyItemIds([
+ bookmarks[0].guid,
+ bookmarks[1].guid,
+ bookmarks[2].guid,
+ ]);
+
+ library = await promiseLibrary("UnfiledBookmarks");
+});
+
+async function run_drag_test(startBookmarkIndex, insertionIndex) {
+ let dragBookmark = bookmarks[startBookmarkIndex];
+
+ library.ContentTree.view.selectItems([dragBookmark.guid]);
+
+ let dataTransfer = {
+ _data: [],
+ dropEffect: "move",
+ mozCursor: "auto",
+ mozItemCount: 1,
+ types: [PlacesUtils.TYPE_X_MOZ_PLACE],
+ mozTypesAt(i) {
+ return [this._data[0].type];
+ },
+ mozGetDataAt(i) {
+ return this._data[0].data;
+ },
+ mozSetDataAt(type, data, index) {
+ this._data.push({
+ type,
+ data,
+ index,
+ });
+ },
+ };
+
+ let event = {
+ dataTransfer,
+ preventDefault() {},
+ stopPropagation() {},
+ };
+
+ library.ContentTree.view.controller.setDataTransfer(event);
+
+ Assert.equal(
+ dataTransfer.mozTypesAt(0),
+ PlacesUtils.TYPE_X_MOZ_PLACE,
+ "Should have x-moz-place as the first data type."
+ );
+
+ let dataObject = JSON.parse(dataTransfer.mozGetDataAt(0));
+
+ Assert.equal(
+ dataObject.itemGuid,
+ dragBookmark.guid,
+ "Should have the correct guid."
+ );
+ Assert.equal(
+ dataObject.title,
+ dragBookmark.title,
+ "Should have the correct title."
+ );
+
+ Assert.equal(dataTransfer.dropEffect, "move");
+
+ let ip = new PlacesInsertionPoint({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: insertionIndex,
+ orientation: Ci.nsITreeView.DROP_ON,
+ });
+
+ await PlacesControllerDragHelper.onDrop(ip, dataTransfer);
+}
+
+add_task(async function test_simple_move_down() {
+ let moveNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-moved",
+ events =>
+ events.some(
+ e => e.guid === bookmarks[0].guid && e.oldIndex == 0 && e.index == 1
+ )
+ );
+
+ await run_drag_test(0, 2);
+
+ await moveNotification;
+});
+
+add_task(async function test_simple_move_up() {
+ await run_drag_test(2, 0);
+});
diff --git a/browser/components/places/tests/browser/browser_controller_onDrop_query.js b/browser/components/places/tests/browser/browser_controller_onDrop_query.js
new file mode 100644
index 0000000000..10dd6faa3c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_controller_onDrop_query.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+
+add_setup(async function () {
+ // Clean before and after so we don't have anything in the folders.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+// Simulating actual drag and drop is hard for a xul tree as we can't get the
+// required source elements, so we have to do a lot more work by hand.
+async function simulateDrop(
+ selectTargets,
+ sourceBm,
+ dropEffect,
+ targetGuid,
+ isVirtualRoot = false
+) {
+ await withSidebarTree("bookmarks", async function (tree) {
+ for (let target of selectTargets) {
+ tree.selectItems([target]);
+ if (tree.selectedNode instanceof Ci.nsINavHistoryContainerResultNode) {
+ tree.selectedNode.containerOpen = true;
+ }
+ }
+
+ let dataTransfer = {
+ _data: [],
+ dropEffect,
+ mozCursor: "auto",
+ mozItemCount: 1,
+ types: [PlacesUtils.TYPE_X_MOZ_PLACE],
+ mozTypesAt(i) {
+ return [this._data[0].type];
+ },
+ mozGetDataAt(i) {
+ return this._data[0].data;
+ },
+ mozSetDataAt(type, data, index) {
+ this._data.push({
+ type,
+ data,
+ index,
+ });
+ },
+ };
+
+ let event = {
+ dataTransfer,
+ preventDefault() {},
+ stopPropagation() {},
+ };
+
+ tree._controller.setDataTransfer(event);
+
+ Assert.equal(
+ dataTransfer.mozTypesAt(0),
+ PlacesUtils.TYPE_X_MOZ_PLACE,
+ "Should have x-moz-place as the first data type."
+ );
+
+ let dataObject = JSON.parse(dataTransfer.mozGetDataAt(0));
+
+ let guid = isVirtualRoot ? dataObject.concreteGuid : dataObject.itemGuid;
+
+ Assert.equal(guid, sourceBm.guid, "Should have the correct guid.");
+ Assert.equal(
+ dataObject.title,
+ PlacesUtils.bookmarks.getLocalizedTitle(sourceBm),
+ "Should have the correct title."
+ );
+
+ Assert.equal(dataTransfer.dropEffect, dropEffect);
+
+ let ip = new PlacesInsertionPoint({
+ parentId: await PlacesTestUtils.promiseItemId(targetGuid),
+ parentGuid: targetGuid,
+ index: 0,
+ orientation: Ci.nsITreeView.DROP_ON,
+ });
+
+ await PlacesControllerDragHelper.onDrop(ip, dataTransfer);
+ });
+}
+
+add_task(async function test_move_out_of_query() {
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Fake",
+ url: TEST_URL,
+ });
+
+ let queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ let queries = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ title: "Query",
+ url: `place:queryType=${queryType}&terms=Fake`,
+ },
+ ],
+ });
+ await simulateDrop(
+ [queries[0].guid, bookmark.guid],
+ bookmark,
+ "move",
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+
+ let newBm = await PlacesUtils.bookmarks.fetch(bookmark.guid);
+
+ Assert.equal(
+ newBm.parentGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ "should have moved the bookmark to a new folder."
+ );
+
+ let oldLocationBm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0,
+ });
+
+ Assert.ok(!oldLocationBm, "Should not have a bookmark at the old location.");
+});
diff --git a/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js b/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js
new file mode 100644
index 0000000000..2637d4d724
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js
@@ -0,0 +1,312 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+
+add_setup(async function () {
+ // The following initialization code is necessary to avoid a frequent
+ // intermittent failure in verify-fission where, due to timings, we may or
+ // may not import default bookmarks.
+ info("Ensure Places init is complete");
+ let placesInitCompleteObserved = TestUtils.topicObserved(
+ "places-browser-init-complete"
+ );
+ Cc["@mozilla.org/browser/browserglue;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "browser-glue-test", "places-browser-init-complete");
+ await placesInitCompleteObserved;
+ // Clean before and after so we don't have anything in the folders.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+// Simulating actual drag and drop is hard for a xul tree as we can't get the
+// required source elements, so we have to do a lot more work by hand.
+async function simulateDrop(
+ selectTargets,
+ sourceBm,
+ dropEffect,
+ targetGuid,
+ isVirtualRoot = false
+) {
+ await withSidebarTree("bookmarks", async function (tree) {
+ for (let target of selectTargets) {
+ tree.selectItems([target]);
+ if (tree.selectedNode instanceof Ci.nsINavHistoryContainerResultNode) {
+ tree.selectedNode.containerOpen = true;
+ }
+ }
+
+ let dataTransfer = {
+ _data: [],
+ dropEffect,
+ mozCursor: "auto",
+ mozItemCount: 1,
+ types: [PlacesUtils.TYPE_X_MOZ_PLACE],
+ mozTypesAt(i) {
+ return [this._data[0].type];
+ },
+ mozGetDataAt(i) {
+ return this._data[0].data;
+ },
+ mozSetDataAt(type, data, index) {
+ this._data.push({
+ type,
+ data,
+ index,
+ });
+ },
+ };
+
+ let event = {
+ dataTransfer,
+ preventDefault() {},
+ stopPropagation() {},
+ };
+
+ tree._controller.setDataTransfer(event);
+
+ Assert.equal(
+ dataTransfer.mozTypesAt(0),
+ PlacesUtils.TYPE_X_MOZ_PLACE,
+ "Should have x-moz-place as the first data type."
+ );
+
+ let dataObject = JSON.parse(dataTransfer.mozGetDataAt(0));
+
+ let guid = isVirtualRoot ? dataObject.concreteGuid : dataObject.itemGuid;
+
+ Assert.equal(guid, sourceBm.guid, "Should have the correct guid.");
+ Assert.equal(
+ dataObject.title,
+ PlacesUtils.bookmarks.getLocalizedTitle(sourceBm),
+ "Should have the correct title."
+ );
+
+ Assert.equal(dataTransfer.dropEffect, dropEffect);
+
+ let ip = new PlacesInsertionPoint({
+ parentId: await PlacesTestUtils.promiseItemId(targetGuid),
+ parentGuid: targetGuid,
+ index: 0,
+ orientation: Ci.nsITreeView.DROP_ON,
+ });
+
+ await PlacesControllerDragHelper.onDrop(ip, dataTransfer);
+ });
+}
+
+add_task(async function test_move_normal_bm_in_sidebar() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "Fake",
+ url: TEST_URL,
+ });
+
+ await simulateDrop([bm.guid], bm, "move", PlacesUtils.bookmarks.unfiledGuid);
+
+ let newBm = await PlacesUtils.bookmarks.fetch(bm.guid);
+
+ Assert.equal(
+ newBm.parentGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ "Should have moved to the new parent."
+ );
+
+ let oldLocationBm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+
+ Assert.ok(!oldLocationBm, "Should not have a bookmark at the old location.");
+});
+
+add_task(async function test_try_move_root_in_sidebar() {
+ let menuFolder = await PlacesUtils.bookmarks.fetch(
+ PlacesUtils.bookmarks.menuGuid
+ );
+ await simulateDrop(
+ [menuFolder.guid],
+ menuFolder,
+ "move",
+ PlacesUtils.bookmarks.toolbarGuid,
+ true
+ );
+
+ menuFolder = await PlacesUtils.bookmarks.fetch(
+ PlacesUtils.bookmarks.menuGuid
+ );
+
+ Assert.equal(
+ menuFolder.parentGuid,
+ PlacesUtils.bookmarks.rootGuid,
+ "Should have remained in the root"
+ );
+
+ let newFolder = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+
+ Assert.notEqual(
+ newFolder.guid,
+ menuFolder.guid,
+ "Should have created a different folder"
+ );
+ Assert.equal(
+ newFolder.title,
+ PlacesUtils.bookmarks.getLocalizedTitle(menuFolder),
+ "Should have copied the folder title."
+ );
+ Assert.equal(
+ newFolder.type,
+ PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ "Should have a bookmark type (for a folder shortcut)."
+ );
+ Assert.equal(
+ newFolder.url,
+ `place:parent=${PlacesUtils.bookmarks.menuGuid}`,
+ "Should have the correct url for the folder shortcut."
+ );
+});
+
+add_task(async function test_try_move_bm_within_two_root_folder_queries() {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Fake",
+ url: TEST_URL,
+ });
+
+ let queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+
+ let queries = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ title: "Query",
+ url: `place:queryType=${queryType}&terms=Fake`,
+ },
+ ],
+ });
+
+ await simulateDrop(
+ [queries[0].guid, bookmark.guid],
+ bookmark,
+ "move",
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+
+ let newBm = await PlacesUtils.bookmarks.fetch(bookmark.guid);
+
+ Assert.equal(
+ newBm.parentGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ "should have moved the bookmark to a new folder."
+ );
+});
+
+add_task(async function test_move_within_itself() {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "bm1",
+ url: "http://www.example.com/bookmark1.html",
+ },
+ {
+ title: "bm2",
+ url: "http://www.example.com/bookmark2.html",
+ },
+ {
+ title: "bm3",
+ url: "http://www.example.com/bookmark3.html",
+ },
+ ],
+ });
+
+ await withSidebarTree("bookmarks", async function (tree) {
+ // Select the folder containing the bookmarks
+ // and save its index position
+ tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
+ let unfiledFolderIndex = tree.view.treeIndexForNode(tree.selectedNode);
+
+ let guids = bookmarks.map(bookmark => bookmark.guid);
+ tree.selectItems(guids);
+ let dataTransfer = {
+ _data: [],
+ dropEffect: "move",
+ mozCursor: "auto",
+ mozItemCount: bookmarks.length,
+ types: [PlacesUtils.TYPE_X_MOZ_PLACE],
+ mozTypesAt(i) {
+ return [this._data[0].type];
+ },
+ mozGetDataAt(i) {
+ return this._data[0].data;
+ },
+ mozSetDataAt(type, data, index) {
+ this._data.push({
+ type,
+ data,
+ index,
+ });
+ },
+ };
+
+ bookmarks.forEach((bookmark, index) => {
+ // Index positions of the newly created bookmarks
+ bookmark.rowIndex = unfiledFolderIndex + index + 1;
+ bookmark.node = tree.view.nodeForTreeIndex(bookmark.rowIndex);
+ bookmark.cachedBookmarkIndex = bookmark.node.bookmarkIndex;
+ });
+
+ let assertBookmarksHaveNotChangedPosition = () => {
+ bookmarks.forEach(bookmark => {
+ Assert.equal(
+ bookmark.node.bookmarkIndex,
+ bookmark.cachedBookmarkIndex,
+ "should not have moved the bookmark."
+ );
+ });
+ };
+
+ // Mimic "drag" events
+ let dragStartEvent = new CustomEvent("dragstart", {
+ bubbles: true,
+ });
+ dragStartEvent.dataTransfer = dataTransfer;
+
+ let dragEndEvent = new CustomEvent("dragend", {
+ bubbles: true,
+ });
+
+ let treeChildren = tree.view._element.children[1];
+
+ treeChildren.dispatchEvent(dragStartEvent);
+ await tree.view.drop(
+ bookmarks[1].rowIndex,
+ Ci.nsITreeView.DROP_ON,
+ dataTransfer
+ );
+ treeChildren.dispatchEvent(dragEndEvent);
+ assertBookmarksHaveNotChangedPosition();
+
+ treeChildren.dispatchEvent(dragStartEvent);
+ await tree.view.drop(
+ bookmarks[2].rowIndex,
+ Ci.nsITreeView.DROP_ON,
+ dataTransfer
+ );
+ treeChildren.dispatchEvent(dragEndEvent);
+ assertBookmarksHaveNotChangedPosition();
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js b/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js
new file mode 100644
index 0000000000..64c448ec3f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js
@@ -0,0 +1,125 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const sandbox = sinon.createSandbox();
+const TAG_NAME = "testTag";
+
+var bookmarks;
+var bookmarkId;
+
+add_setup(async function () {
+ registerCleanupFunction(async function () {
+ sandbox.restore();
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+
+ sandbox.stub(PlacesTransactions, "batch");
+ sandbox.stub(PlacesTransactions, "Tag");
+
+ bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "bm1",
+ url: "http://example1.com",
+ },
+ {
+ title: "bm2",
+ url: "http://example2.com",
+ tags: [TAG_NAME],
+ },
+ ],
+ });
+ bookmarkId = await PlacesTestUtils.promiseItemId(bookmarks[0].guid);
+});
+
+async function run_drag_test(startBookmarkIndex, newParentGuid) {
+ if (!newParentGuid) {
+ newParentGuid = PlacesUtils.bookmarks.unfiledGuid;
+ }
+
+ // Reset the stubs so that previous test runs don't count against us.
+ PlacesTransactions.Tag.reset();
+ PlacesTransactions.batch.reset();
+
+ let dragBookmark = bookmarks[startBookmarkIndex];
+
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([PlacesUtils.bookmarks.unfiledGuid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+
+ // Simulating a drag-drop with a tree view turns out to be really difficult
+ // as you can't get a node for the source/target. Hence, we fake the
+ // insertion point and drag data and call the function direct.
+ let ip = new PlacesInsertionPoint({
+ isTag: true,
+ tagName: TAG_NAME,
+ orientation: Ci.nsITreeView.DROP_ON,
+ });
+
+ let bookmarkWithId = JSON.stringify(
+ Object.assign(
+ {
+ id: bookmarkId,
+ itemGuid: dragBookmark.guid,
+ uri: dragBookmark.url,
+ },
+ dragBookmark
+ )
+ );
+
+ let dt = {
+ dropEffect: "move",
+ mozCursor: "auto",
+ mozItemCount: 1,
+ types: [PlacesUtils.TYPE_X_MOZ_PLACE],
+ mozTypesAt(i) {
+ return this.types;
+ },
+ mozGetDataAt(i) {
+ return bookmarkWithId;
+ },
+ };
+
+ await PlacesControllerDragHelper.onDrop(ip, dt);
+
+ Assert.ok(
+ PlacesTransactions.Tag.calledOnce,
+ "Should have called PlacesTransactions.Tag at least once."
+ );
+
+ let arg = PlacesTransactions.Tag.args[0][0];
+
+ Assert.equal(
+ arg.urls.length,
+ 1,
+ "Should have called PlacesTransactions.Tag with an array of one url"
+ );
+ Assert.equal(
+ arg.urls[0],
+ dragBookmark.url,
+ "Should have called PlacesTransactions.Tag with the correct url"
+ );
+ Assert.equal(
+ arg.tag,
+ TAG_NAME,
+ "Should have called PlacesTransactions.Tag with the correct tag name"
+ );
+ });
+}
+
+add_task(async function test_simple_drop_and_tag() {
+ // When we move items down the list, we'll get a drag index that is one higher
+ // than where we actually want to insert to - as the item is being moved up,
+ // everything shifts down one. Hence the index to pass to the transaction should
+ // be one less than the supplied index.
+ await run_drag_test(0, PlacesUtils.bookmarks.tagGuid);
+});
diff --git a/browser/components/places/tests/browser/browser_copy_query_without_tree.js b/browser/components/places/tests/browser/browser_copy_query_without_tree.js
new file mode 100644
index 0000000000..fc3adf31f2
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_copy_query_without_tree.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* test that copying a non movable query or folder shortcut makes a new query with the same url, not a deep copy */
+
+const QUERY_URL = "place:sort=8&maxResults=10";
+
+add_task(async function copy_toolbar_shortcut() {
+ await promisePlacesInitComplete();
+
+ let library = await promiseLibrary();
+
+ registerCleanupFunction(async () => {
+ library.close();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+
+ await promiseClipboard(function () {
+ library.PlacesOrganizer._places.controller.copy();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ await library.ContentTree.view.controller.paste();
+
+ let toolbarCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(
+ toolbarCopyNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "copy is still a folder shortcut"
+ );
+
+ await PlacesUtils.bookmarks.remove(toolbarCopyNode.bookmarkGuid);
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+ is(
+ library.PlacesOrganizer._places.selectedNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "original is still a folder shortcut"
+ );
+});
+
+add_task(async function copy_mobile_shortcut() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.bookmarks.showMobileBookmarks", true]],
+ });
+ await promisePlacesInitComplete();
+
+ let library = await promiseLibrary();
+
+ registerCleanupFunction(async () => {
+ library.close();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ library.PlacesOrganizer.selectLeftPaneContainerByHierarchy([
+ PlacesUtils.virtualAllBookmarksGuid,
+ PlacesUtils.bookmarks.virtualMobileGuid,
+ ]);
+
+ await promiseClipboard(function () {
+ library.PlacesOrganizer._places.controller.copy();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ await library.ContentTree.view.controller.paste();
+
+ let mobileCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(
+ mobileCopyNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "copy is still a folder shortcut"
+ );
+
+ await PlacesUtils.bookmarks.remove(mobileCopyNode.bookmarkGuid);
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+ is(
+ library.PlacesOrganizer._places.selectedNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_FOLDER_SHORTCUT,
+ "original is still a folder shortcut"
+ );
+});
+
+add_task(async function copy_history_query() {
+ let library = await promiseLibrary();
+
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("History");
+
+ await promiseClipboard(function () {
+ library.PlacesOrganizer._places.controller.copy();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ await library.ContentTree.view.controller.paste();
+
+ let historyCopyNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ is(
+ historyCopyNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
+ "copy is still a query"
+ );
+
+ await PlacesUtils.bookmarks.remove(historyCopyNode.bookmarkGuid);
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("History");
+ is(
+ library.PlacesOrganizer._places.selectedNode.type,
+ Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY,
+ "original is still a query"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_cutting_bookmarks.js b/browser/components/places/tests/browser/browser_cutting_bookmarks.js
new file mode 100644
index 0000000000..7c10229f7e
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_cutting_bookmarks.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "http://example.com/";
+
+add_task(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ let organizer = await promiseLibrary();
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(organizer);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ let ContentTree = organizer.ContentTree;
+
+ // Test with multiple entries to ensure they retain their order.
+ let bookmarks = [];
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "0",
+ })
+ );
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "1",
+ })
+ );
+ bookmarks.push(
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URL,
+ title: "2",
+ })
+ );
+
+ await selectBookmarksIn(organizer, bookmarks, "BookmarksToolbar");
+
+ await promiseClipboard(() => {
+ info("Cutting selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ await selectBookmarksIn(organizer, bookmarks, "UnfiledBookmarks");
+});
+
+var selectBookmarksIn = async function (organizer, bookmarks, aLeftPaneQuery) {
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ let ContentTree = organizer.ContentTree;
+ info("Selecting " + aLeftPaneQuery + " in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn(aLeftPaneQuery);
+
+ for (let { guid } of bookmarks) {
+ let bookmark = await PlacesUtils.bookmarks.fetch(guid);
+ is(
+ bookmark.parentGuid,
+ PlacesUtils.getConcreteItemGuid(PlacesOrganizer._places.selectedNode),
+ "Bookmark has the right parent"
+ );
+ }
+
+ info("Selecting the bookmarks in the right pane");
+ ContentTree.view.selectItems(bookmarks.map(bm => bm.guid));
+
+ for (let node of ContentTree.view.selectedNodes) {
+ is(
+ "" + node.bookmarkIndex,
+ node.title,
+ "Found the expected bookmark in the expected position"
+ );
+ }
+};
diff --git a/browser/components/places/tests/browser/browser_default_bookmark_location.js b/browser/components/places/tests/browser/browser_default_bookmark_location.js
new file mode 100644
index 0000000000..9b386fd2f0
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_default_bookmark_location.js
@@ -0,0 +1,274 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const LOCATION_PREF = "browser.bookmarks.defaultLocation";
+const TEST_URL = "about:about";
+let bookmarkPanel;
+let win;
+
+add_setup(async function () {
+ Services.prefs.clearUserPref(LOCATION_PREF);
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ win = await BrowserTestUtils.openNewBrowserWindow();
+ await BrowserTestUtils.openNewForegroundTab({
+ gBrowser: win.gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ let oldTimeout = win.StarUI._autoCloseTimeout;
+ // Make the timeout something big, so it doesn't interact badly with tests.
+ win.StarUI._autoCloseTimeout = 6000000;
+
+ win.StarUI._createPanelIfNeeded();
+ bookmarkPanel = win.document.getElementById("editBookmarkPanel");
+ bookmarkPanel.setAttribute("animate", false);
+
+ registerCleanupFunction(async () => {
+ bookmarkPanel = null;
+ win.StarUI._autoCloseTimeout = oldTimeout;
+ await BrowserTestUtils.closeWindow(win);
+ win = null;
+ await PlacesUtils.bookmarks.eraseEverything();
+ Services.prefs.clearUserPref(LOCATION_PREF);
+ });
+});
+
+async function cancelBookmarkCreationInPanel() {
+ let hiddenPromise = promisePopupHidden(
+ win.document.getElementById("editBookmarkPanel")
+ );
+ // Confirm and close the dialog.
+
+ win.document.getElementById("editBookmarkPanelRemoveButton").click();
+ await hiddenPromise;
+}
+
+/**
+ * Helper to check the selected folder is correct.
+ */
+async function checkSelection() {
+ // Open folder selector.
+ let menuList = win.document.getElementById("editBMPanel_folderMenuList");
+
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ Assert.equal(
+ menuList.label,
+ PlacesUtils.getString(expectedFolder),
+ `Should have ${expectedFolder} selected by default`
+ );
+ Assert.equal(
+ menuList.getAttribute("selectedGuid"),
+ await PlacesUIUtils.defaultParentGuid,
+ "Should have the correct default guid selected"
+ );
+
+ await cancelBookmarkCreationInPanel();
+}
+
+/**
+ * Verify that bookmarks created with the star button go to the default
+ * bookmark location.
+ */
+add_task(async function test_star_location() {
+ await clickBookmarkStar(win);
+ await checkSelection();
+});
+
+/**
+ * Verify that bookmarks created with the shortcut go to the default bookmark
+ * location.
+ */
+add_task(async function test_shortcut_location() {
+ let shownPromise = promisePopupShown(
+ win.document.getElementById("editBookmarkPanel")
+ );
+ win.document.getElementById("Browser:AddBookmarkAs").doCommand();
+ await shownPromise;
+ await checkSelection();
+});
+
+// Note: Bookmarking frames is tested in browser_addBookmarkForFrame.js
+
+/**
+ * Verify that bookmarks created with the link context menu go to the default
+ * bookmark location.
+ */
+add_task(async function test_context_menu_link() {
+ for (let t = 0; t < 2; t++) {
+ if (t == 1) {
+ // For the second iteration, ensure that the default folder is invalid first.
+ await createAndRemoveDefaultFolder();
+ }
+
+ await withBookmarksDialog(
+ true,
+ async function openDialog() {
+ const contextMenu = win.document.getElementById(
+ "contentAreaContextMenu"
+ );
+ is(contextMenu.state, "closed", "checking if popup is closed");
+ let promisePopupShown = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "a[href*=config]", // Bookmark about:config
+ { type: "contextmenu", button: 2 },
+ win.gBrowser.selectedBrowser
+ );
+ await promisePopupShown;
+ contextMenu.activateItem(
+ win.document.getElementById("context-bookmarklink")
+ );
+ },
+ async function test(dialogWin) {
+ let expectedFolder = "BookmarksToolbarFolderTitle";
+ let expectedFolderName = PlacesUtils.getString(expectedFolder);
+
+ let folderPicker = dialogWin.document.getElementById(
+ "editBMPanel_folderMenuList"
+ );
+
+ // Check the initial state of the folder picker.
+ await TestUtils.waitForCondition(
+ () => folderPicker.selectedItem.label == expectedFolderName,
+ "The folder is the expected one."
+ );
+ }
+ );
+ }
+});
+
+/**
+ * Verify that if we change the location, we persist that selection.
+ */
+add_task(async function test_change_location_panel() {
+ await clickBookmarkStar(win);
+
+ let menuList = win.document.getElementById("editBMPanel_folderMenuList");
+
+ let { toolbarGuid, menuGuid, unfiledGuid } = PlacesUtils.bookmarks;
+
+ let expectedFolderGuid = toolbarGuid;
+
+ info("Pref value: " + Services.prefs.getCharPref(LOCATION_PREF, ""));
+ await TestUtils.waitForCondition(
+ () => menuList.getAttribute("selectedGuid") == expectedFolderGuid,
+ "Should initially select the unfiled or toolbar item"
+ );
+
+ // Now move this new bookmark to the menu:
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ menuList.menupopup,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(menuList, {}, win);
+ await promisePopup;
+
+ // Make sure we wait for the bookmark to be added.
+ let itemAddedPromise = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(({ url }) => url === TEST_URL)
+ );
+
+ // Wait for the pref to change
+ let prefChangedPromise = TestUtils.waitForPrefChange(LOCATION_PREF);
+
+ // Click the choose item.
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.getElementById("editBMPanel_bmRootItem"),
+ {},
+ win
+ );
+
+ await TestUtils.waitForCondition(
+ () => menuList.getAttribute("selectedGuid") == menuGuid,
+ "Should select the menu folder item"
+ );
+
+ info("Waiting for transactions to finish.");
+ await Promise.all(win.gEditItemOverlay.transactionPromises);
+ info("Moved; waiting to hide panel.");
+
+ await hideBookmarksPanel(win);
+ info("Waiting for pref change.");
+ await prefChangedPromise;
+ info("Waiting for item to be added.");
+ await itemAddedPromise;
+
+ // Check that it's in the menu, and remove the bookmark:
+ let bm = await PlacesUtils.bookmarks.fetch({ url: TEST_URL });
+ is(bm?.parentGuid, menuGuid, "Bookmark was put in the menu.");
+ if (bm) {
+ await PlacesUtils.bookmarks.remove(bm);
+ }
+
+ // Now create a new bookmark and check it starts in the menu
+ await clickBookmarkStar(win);
+
+ let expectedFolder = "BookmarksMenuFolderTitle";
+ Assert.equal(
+ menuList.label,
+ PlacesUtils.getString(expectedFolder),
+ `Should have menu folder selected by default`
+ );
+ Assert.equal(
+ menuList.getAttribute("selectedGuid"),
+ menuGuid,
+ "Should have the correct default guid selected"
+ );
+
+ // Now select a different item.
+ promisePopup = BrowserTestUtils.waitForEvent(
+ menuList.menupopup,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(menuList, {}, win);
+ await promisePopup;
+
+ // Click the toolbar item.
+ EventUtils.synthesizeMouseAtCenter(
+ win.document.getElementById("editBMPanel_toolbarFolderItem"),
+ {},
+ win
+ );
+
+ await TestUtils.waitForCondition(
+ () => menuList.getAttribute("selectedGuid") == toolbarGuid,
+ "Should select the toolbar item"
+ );
+
+ await cancelBookmarkCreationInPanel();
+
+ is(
+ await PlacesUIUtils.defaultParentGuid,
+ menuGuid,
+ "Default folder should not change if we cancel the panel."
+ );
+
+ // Now open the panel for an existing bookmark whose parent doesn't match
+ // the default and check we don't overwrite the default folder.
+ let testBM = await PlacesUtils.bookmarks.insert({
+ parentGuid: unfiledGuid,
+ title: "Yoink",
+ url: TEST_URL,
+ });
+ await TestUtils.waitForCondition(
+ () => win.BookmarkingUI.star.hasAttribute("starred"),
+ "Wait for bookmark to show up for current page."
+ );
+ await clickBookmarkStar(win);
+
+ await hideBookmarksPanel(win);
+ is(
+ await PlacesUIUtils.defaultParentGuid,
+ menuGuid,
+ "Default folder should not change if we accept the panel, but didn't change folders."
+ );
+
+ await PlacesUtils.bookmarks.remove(testBM);
+});
diff --git a/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
new file mode 100644
index 0000000000..ea96282f30
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js
@@ -0,0 +1,255 @@
+/* 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://www.mozilla.org";
+const TEST_TITLE = "example_title";
+
+var gBookmarksToolbar = window.document.getElementById("PersonalToolbar");
+var dragDirections = { LEFT: 0, UP: 1, RIGHT: 2, DOWN: 3 };
+
+/**
+ * Tests dragging on toolbar.
+ *
+ * We must test these 2 cases:
+ * - Dragging toward left, top, right should start a drag.
+ * - Dragging toward down should should open the container if the item is a
+ * container, drag the item otherwise.
+ *
+ * @param {object} aElement
+ * DOM node element we will drag
+ * @param {Array} aExpectedDragData
+ * Array of flavors and values in the form:
+ * [ ["text/plain: sometext", "text/html: <b>sometext</b>"], [...] ]
+ * Pass an empty array to check that drag even has been canceled.
+ * @param {number} aDirection
+ * Direction for the dragging gesture, see dragDirections helper object.
+ * @returns {Promise} Resolved once the drag gesture has been observed.
+ */
+function synthesizeDragWithDirection(aElement, aExpectedDragData, aDirection) {
+ let promise = new Promise(resolve => {
+ // Dragstart listener function.
+ gBookmarksToolbar.addEventListener("dragstart", function listener(event) {
+ info("A dragstart event has been trapped.");
+ var dataTransfer = event.dataTransfer;
+ is(
+ dataTransfer.mozItemCount,
+ aExpectedDragData.length,
+ "Number of dragged items should be the same."
+ );
+
+ for (var t = 0; t < dataTransfer.mozItemCount; t++) {
+ var types = dataTransfer.mozTypesAt(t);
+ var expecteditem = aExpectedDragData[t];
+ is(
+ types.length,
+ expecteditem.length,
+ "Number of flavors for item " + t + " should be the same."
+ );
+
+ for (var f = 0; f < types.length; f++) {
+ is(
+ types[f],
+ expecteditem[f].substring(0, types[f].length),
+ "Flavor " + types[f] + " for item " + t + " should be the same."
+ );
+ is(
+ dataTransfer.mozGetDataAt(types[f], t),
+ expecteditem[f].substring(types[f].length + 2),
+ "Contents for item " +
+ t +
+ " with flavor " +
+ types[f] +
+ " should be the same."
+ );
+ }
+ }
+
+ if (!aExpectedDragData.length) {
+ ok(event.defaultPrevented, "Drag has been canceled.");
+ }
+
+ event.preventDefault();
+ event.stopPropagation();
+
+ gBookmarksToolbar.removeEventListener("dragstart", listener);
+
+ // This is likely to cause a click event, and, in case we are dragging a
+ // bookmark, an unwanted page visit. Prevent the click event.
+ aElement.addEventListener("click", prevent);
+ EventUtils.synthesizeMouse(
+ aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mouseup" }
+ );
+ aElement.removeEventListener("click", prevent);
+
+ // Cleanup eventually opened menus.
+ if (aElement.localName == "menu" && aElement.open) {
+ aElement.open = false;
+ }
+ resolve();
+ });
+ });
+
+ var prevent = function (aEvent) {
+ aEvent.preventDefault();
+ };
+
+ var xIncrement = 0;
+ var yIncrement = 0;
+
+ switch (aDirection) {
+ case dragDirections.LEFT:
+ xIncrement = -1;
+ break;
+ case dragDirections.RIGHT:
+ xIncrement = +1;
+ break;
+ case dragDirections.UP:
+ yIncrement = -1;
+ break;
+ case dragDirections.DOWN:
+ yIncrement = +1;
+ break;
+ }
+
+ var rect = aElement.getBoundingClientRect();
+ var startingPoint = {
+ x: (rect.right - rect.left) / 2,
+ y: (rect.bottom - rect.top) / 2,
+ };
+
+ EventUtils.synthesizeMouse(aElement, startingPoint.x, startingPoint.y, {
+ type: "mousedown",
+ });
+ EventUtils.synthesizeMouse(
+ aElement,
+ startingPoint.x + xIncrement * 1,
+ startingPoint.y + yIncrement * 1,
+ { type: "mousemove" }
+ );
+ EventUtils.synthesizeMouse(
+ aElement,
+ startingPoint.x + xIncrement * 9,
+ startingPoint.y + yIncrement * 9,
+ { type: "mousemove" }
+ );
+
+ return promise;
+}
+
+function getToolbarNodeForItemId(itemGuid) {
+ var children = document.getElementById("PlacesToolbarItems").children;
+ for (let child of children) {
+ if (itemGuid == child._placesNode.bookmarkGuid) {
+ return child;
+ }
+ }
+ return null;
+}
+
+function getExpectedDataForPlacesNode(aNode) {
+ var wrappedNode = [];
+ var flavors = [
+ "text/x-moz-place",
+ "text/x-moz-url",
+ "text/plain",
+ "text/html",
+ ];
+
+ flavors.forEach(function (aFlavor) {
+ var wrappedFlavor = aFlavor + ": " + PlacesUtils.wrapNode(aNode, aFlavor);
+ wrappedNode.push(wrappedFlavor);
+ });
+
+ return [wrappedNode];
+}
+
+add_setup(async function () {
+ var toolbar = document.getElementById("PersonalToolbar");
+ var wasCollapsed = toolbar.collapsed;
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ registerCleanupFunction(async () => {
+ // Collapse the personal toolbar if needed.
+ await promiseSetToolbarVisibility(toolbar, false);
+ });
+});
+
+add_task(async function test_drag_folder_on_toolbar() {
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: TEST_TITLE,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+
+ var element = getToolbarNodeForItemId(folder.guid);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(
+ element._placesNode,
+ null,
+ "Toolbar node has an associated Places node."
+ );
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ info("Dragging left");
+ await synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT);
+
+ info("Dragging right");
+ await synthesizeDragWithDirection(
+ element,
+ expectedData,
+ dragDirections.RIGHT
+ );
+
+ info("Dragging up");
+ await synthesizeDragWithDirection(element, expectedData, dragDirections.UP);
+
+ info("Dragging down");
+ await synthesizeDragWithDirection(element, [], dragDirections.DOWN);
+
+ await PlacesUtils.bookmarks.remove(folder);
+});
+
+add_task(async function test_drag_bookmark_on_toolbar() {
+ var bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: TEST_TITLE,
+ url: TEST_URL,
+ });
+
+ var element = getToolbarNodeForItemId(bookmark.guid);
+ isnot(element, null, "Found node on toolbar");
+
+ isnot(
+ element._placesNode,
+ null,
+ "Toolbar node has an associated Places node."
+ );
+ var expectedData = getExpectedDataForPlacesNode(element._placesNode);
+
+ info("Dragging left");
+ await synthesizeDragWithDirection(element, expectedData, dragDirections.LEFT);
+
+ info("Dragging right");
+ await synthesizeDragWithDirection(
+ element,
+ expectedData,
+ dragDirections.RIGHT
+ );
+
+ info("Dragging up");
+ await synthesizeDragWithDirection(element, expectedData, dragDirections.UP);
+
+ info("Dragging down");
+ synthesizeDragWithDirection(element, expectedData, dragDirections.DOWN);
+
+ await PlacesUtils.bookmarks.remove(bookmark);
+});
diff --git a/browser/components/places/tests/browser/browser_drag_folder_on_newTab.js b/browser/components/places/tests/browser/browser_drag_folder_on_newTab.js
new file mode 100644
index 0000000000..9f12e57ee9
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_drag_folder_on_newTab.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+add_setup(async function () {
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ // Clean before and after so we don't have anything in the folders.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ registerCleanupFunction(async function () {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+const TEST_FOLDER_NAME = "Test folder";
+
+add_task(async function test_change_location_from_Toolbar() {
+ let newTabButton = document.getElementById("tabs-newtab-button");
+
+ let children = [
+ {
+ title: "first",
+ url: "http://www.example.com/first",
+ },
+ {
+ title: "second",
+ url: "http://www.example.com/second",
+ },
+ {
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ },
+ {
+ title: "third",
+ url: "http://www.example.com/third",
+ },
+ ];
+ let guid = PlacesUtils.history.makeGuid();
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ guid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: TEST_FOLDER_NAME,
+ children,
+ },
+ ],
+ });
+
+ let folder = getToolbarNodeForItemGuid(guid);
+
+ let loadedPromises = children
+ .filter(item => "url" in item)
+ .map(item =>
+ BrowserTestUtils.waitForNewTab(gBrowser, item.url, true, true)
+ );
+
+ let srcX = 10,
+ srcY = 10;
+ // We should drag upwards, since dragging downwards opens menu instead.
+ let stepX = 0,
+ stepY = -5;
+
+ // We need to dispatch mousemove before dragging, to populate
+ // PlacesToolbar._cachedMouseMoveEvent, with the cursor position after the
+ // first step, so that the places code detects it as dragging upward.
+ EventUtils.synthesizeMouse(folder, srcX + stepX, srcY + stepY, {
+ type: "mousemove",
+ });
+
+ await EventUtils.synthesizePlainDragAndDrop({
+ srcElement: folder,
+ destElement: newTabButton,
+ srcX,
+ srcY,
+ stepX,
+ stepY,
+ });
+
+ let tabs = await Promise.all(loadedPromises);
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ ok(true);
+});
diff --git a/browser/components/places/tests/browser/browser_editBookmark_keywords.js b/browser/components/places/tests/browser/browser_editBookmark_keywords.js
new file mode 100644
index 0000000000..5489a06165
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_editBookmark_keywords.js
@@ -0,0 +1,64 @@
+"use strict";
+
+const TEST_URL = "about:blank";
+
+add_task(async function () {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ opening: TEST_URL,
+ waitForStateStop: true,
+ });
+
+ let library = await promiseLibrary("UnfiledBookmarks");
+ registerCleanupFunction(async () => {
+ await promiseLibraryClosed(library);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await BrowserTestUtils.removeTab(tab);
+ });
+
+ let keywordField = library.document.getElementById(
+ "editBMPanel_keywordField"
+ );
+
+ for (let i = 0; i < 2; ++i) {
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: `http://www.test${i}.me/`,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ let node = library.ContentTree.view.view.nodeForTreeIndex(i);
+ is(node.bookmarkGuid, bm.guid, "Found the expected bookmark");
+ // Select the bookmark.
+ library.ContentTree.view.selectNode(node);
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view);
+
+ is(
+ library.document.getElementById("editBMPanel_keywordField").value,
+ "",
+ "The keyword field should be empty"
+ );
+ info("Add a keyword to the bookmark");
+ const promise = PlacesTestUtils.waitForNotification(
+ "bookmark-keyword-changed"
+ );
+ keywordField.focus();
+ keywordField.value = "kw";
+ EventUtils.sendString(i.toString(), library);
+ keywordField.blur();
+ const events = await promise;
+ is(events.length, 1, "Number of events fired is correct");
+ const keyword = events[0].keyword;
+ is(keyword, `kw${i}`, "The new keyword value is correct");
+ }
+
+ for (let i = 0; i < 2; ++i) {
+ let entry = await PlacesUtils.keywords.fetch({
+ url: `http://www.test${i}.me/`,
+ });
+ is(
+ entry.keyword,
+ `kw${i}`,
+ `The keyword for http://www.test${i}.me/ is correct`
+ );
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js b/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js
new file mode 100644
index 0000000000..8d4d650984
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that the Bookmarks Toolbar and Sidebar can be enabled from the Bookmarks Menu ("View history,
+ * saved bookmarks, and more" button.
+ */
+
+// Cleanup.
+registerCleanupFunction(async () => {
+ CustomizableUI.setToolbarVisibility("PersonalToolbar", false);
+ CustomizableUI.removeWidgetFromArea("library-button");
+ SidebarUI.hide();
+});
+
+async function selectAppMenuView(buttonId, viewId) {
+ let btn;
+ await TestUtils.waitForCondition(() => {
+ btn = document.getElementById(buttonId);
+ return btn;
+ }, "Should have the " + buttonId + " button");
+ btn.click();
+ let view = document.getElementById(viewId);
+ let viewPromise = BrowserTestUtils.waitForEvent(view, "ViewShown");
+ await viewPromise;
+}
+
+async function openBookmarkingPanelInLibraryToolbarButton() {
+ await selectAppMenuView("library-button", "appMenu-libraryView");
+ await selectAppMenuView(
+ "appMenu-library-bookmarks-button",
+ "PanelUI-bookmarks"
+ );
+}
+
+add_task(async function test_enable_toolbar() {
+ CustomizableUI.addWidgetToArea("library-button", "nav-bar");
+
+ await openBookmarkingPanelInLibraryToolbarButton();
+ let toolbar = document.getElementById("PersonalToolbar");
+ Assert.ok(toolbar.collapsed, "Bookmarks Toolbar is hidden");
+
+ let viewBookmarksToolbarBtn;
+ await TestUtils.waitForCondition(() => {
+ viewBookmarksToolbarBtn = document.getElementById(
+ "panelMenu_viewBookmarksToolbar"
+ );
+ return viewBookmarksToolbarBtn;
+ }, "Should have the library 'View Bookmarks Toolbar' button.");
+ viewBookmarksToolbarBtn.click();
+ await TestUtils.waitForCondition(
+ () => !toolbar.collapsed,
+ "Should have the Bookmarks Toolbar enabled."
+ );
+ Assert.ok(!toolbar.collapsed, "Bookmarks Toolbar is enabled");
+});
diff --git a/browser/components/places/tests/browser/browser_forgetthissite.js b/browser/components/places/tests/browser/browser_forgetthissite.js
new file mode 100644
index 0000000000..380497aed7
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_forgetthissite.js
@@ -0,0 +1,262 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+// Tests the "Forget About This Site" button from the libary view
+const { PromptTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/PromptTestUtils.sys.mjs"
+);
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const { ForgetAboutSite } = ChromeUtils.importESModule(
+ "resource://gre/modules/ForgetAboutSite.sys.mjs"
+);
+
+const TEST_URIs = [
+ { title: "0", uri: "http://example.com" },
+ { title: "1", uri: "http://www.mozilla.org/test1" },
+ { title: "2", uri: "http://www.mozilla.org/test2" },
+ { title: "3", uri: "https://192.168.200.1/login.html" },
+];
+
+async function setup() {
+ registerCleanupFunction(async function () {
+ // Clean up any leftover stubs.
+ sinon.restore();
+ });
+
+ let places = [];
+ let transition = PlacesUtils.history.TRANSITION_TYPED;
+ TEST_URIs.forEach(({ title, uri }) =>
+ places.push({ uri: Services.io.newURI(uri), transition, title })
+ );
+ await PlacesTestUtils.addVisits(places);
+}
+
+async function teardown(organizer) {
+ // Close the library window.
+ await promiseLibraryClosed(organizer);
+ await PlacesUtils.history.clear();
+}
+
+// Selects the sites specified by sitesToSelect
+// If multiple sites are selected they can't be forgotten
+// Should forget selects the answer in the confirmation dialogue
+// removedEntries specifies which entries should be forgotten
+async function testForgetAboutThisSite(
+ sitesToSelect,
+ shouldForget,
+ removedEntries,
+ cancelConfirmWithEsc = false
+) {
+ if (cancelConfirmWithEsc) {
+ ok(
+ !shouldForget,
+ "If cancelConfirmWithEsc is set we don't expect to clear entries."
+ );
+ }
+
+ ok(PlacesUtils, "checking PlacesUtils, running in chrome context?");
+ await setup();
+ let organizer = await promiseHistoryView();
+ let doc = organizer.document;
+ let tree = doc.getElementById("placeContent");
+
+ //Sort by name in descreasing order
+ tree.view._result.sortingMode =
+ Ci.nsINavHistoryQueryOptions.SORT_BY_TITLE_ASCENDING;
+
+ let selection = tree.view.selection;
+ selection.clearSelection();
+ sitesToSelect.forEach(index => selection.rangedSelect(index, index, true));
+
+ let selectionCount = sitesToSelect.length;
+ is(
+ selection.count,
+ selectionCount,
+ "The selected range is as big as expected"
+ );
+ // Open the context menu.
+ let contextmenu = doc.getElementById("placesContext");
+ let popupShown = promisePopupShown(contextmenu);
+
+ // Get cell coordinates.
+ let rect = tree.getCoordsForCellItem(
+ sitesToSelect[0],
+ tree.columns[0],
+ "text"
+ );
+ // Initiate a context menu for the selected cell.
+ EventUtils.synthesizeMouse(
+ tree.body,
+ rect.x + rect.width / 2,
+ rect.y + rect.height / 2,
+ { type: "contextmenu", button: 2 },
+ organizer
+ );
+ await popupShown;
+
+ let forgetThisSite = doc.getElementById("placesContext_deleteHost");
+ let hideForgetThisSite = selectionCount > 1;
+ is(
+ forgetThisSite.hidden,
+ hideForgetThisSite,
+ `The Forget this site menu item should ${
+ hideForgetThisSite ? "" : "not "
+ }be hidden with ${selectionCount} items selected`
+ );
+ if (hideForgetThisSite) {
+ // Close the context menu.
+ contextmenu.hidePopup();
+ await teardown(organizer);
+ return;
+ }
+
+ // Resolves once the confirmation prompt has been closed.
+ let promptPromise;
+
+ // Cancel prompt via esc key. We have to get the prompt closed promise
+ // ourselves.
+ if (cancelConfirmWithEsc) {
+ promptPromise = PromptTestUtils.waitForPrompt(organizer, {
+ modalType: Services.prompt.MODAL_TYPE_WINDOW,
+ promptType: "confirmEx",
+ }).then(dialog => {
+ let dialogWindow = dialog.ui.prompt;
+ let dialogClosedPromise = BrowserTestUtils.waitForEvent(
+ dialogWindow.opener,
+ "DOMModalDialogClosed"
+ );
+ EventUtils.synthesizeKey("KEY_Escape", undefined, dialogWindow);
+
+ return dialogClosedPromise;
+ });
+ } else {
+ // Close prompt via buttons. PromptTestUtils supplies the closed promise.
+ promptPromise = PromptTestUtils.handleNextPrompt(
+ organizer,
+ { modalType: Services.prompt.MODAL_TYPE_WINDOW, promptType: "confirmEx" },
+ { buttonNumClick: shouldForget ? 0 : 1 }
+ );
+ }
+
+ // If we cancel the prompt, create stubs to check that none of the clear
+ // methods are called.
+ if (!shouldForget) {
+ sinon.stub(ForgetAboutSite, "removeDataFromBaseDomain").resolves();
+ sinon.stub(ForgetAboutSite, "removeDataFromDomain").resolves();
+ }
+
+ let pageRemovedEventPromise;
+ if (shouldForget) {
+ pageRemovedEventPromise =
+ PlacesTestUtils.waitForNotification("page-removed");
+ }
+
+ // Execute the delete command.
+ contextmenu.activateItem(forgetThisSite);
+
+ // Wait for prompt to be handled.
+ await promptPromise;
+
+ // If we expect to remove items, wait the page-removed event to fire. If we
+ // don't wait, we may test the list before any items have been removed.
+ await pageRemovedEventPromise;
+
+ if (!shouldForget) {
+ ok(
+ ForgetAboutSite.removeDataFromBaseDomain.notCalled &&
+ ForgetAboutSite.removeDataFromDomain.notCalled,
+ "Should not call ForgetAboutSite when the confirmation prompt is cancelled."
+ );
+ // Remove the stubs.
+ sinon.restore();
+ }
+
+ // Check that the entries have been removed.
+ await Promise.all(
+ removedEntries.map(async ({ uri }) => {
+ Assert.ok(
+ !(await PlacesUtils.history.fetch(uri)),
+ `History entry for ${uri} has been correctly removed`
+ );
+ })
+ );
+ await Promise.all(
+ TEST_URIs.filter(x => !removedEntries.includes(x)).map(async ({ uri }) => {
+ Assert.ok(
+ await PlacesUtils.history.fetch(uri),
+ `History entry for ${uri} has been kept`
+ );
+ })
+ );
+
+ // Cleanup.
+ await teardown(organizer);
+}
+
+/*
+ * Opens the history view in the PlacesOrganziner window
+ * @returns {Promise}
+ * @resolves The PlacesOrganizer
+ */
+async function promiseHistoryView() {
+ let organizer = await promiseLibrary();
+
+ // Select History in the left pane.
+ let po = organizer.PlacesOrganizer;
+ po.selectLeftPaneBuiltIn("History");
+
+ let histContainer = po._places.selectedNode.QueryInterface(
+ Ci.nsINavHistoryContainerResultNode
+ );
+ histContainer.containerOpen = true;
+ po._places.selectNode(histContainer.getChild(0));
+
+ return organizer;
+}
+/*
+ * @returns {Promise}
+ * @resolves once the popup is shown
+ */
+function promisePopupShown(popup) {
+ return new Promise(resolve => {
+ popup.addEventListener(
+ "popupshown",
+ function () {
+ resolve();
+ },
+ { capture: true, once: true }
+ );
+ });
+}
+
+// This test makes sure that the Forget This Site command is hidden for multiple
+// selections.
+add_task(async function selectMultiple() {
+ await testForgetAboutThisSite([0, 1]);
+});
+
+// This test makes sure that forgetting "http://www.mozilla.org/test2" also removes "http://www.mozilla.org/test1"
+add_task(async function forgettingBasedomain() {
+ await testForgetAboutThisSite([1], true, TEST_URIs.slice(1, 3));
+});
+
+// This test makes sure that forgetting by IP address works
+add_task(async function forgettingIPAddress() {
+ await testForgetAboutThisSite([3], true, TEST_URIs.slice(3, 4));
+});
+
+// This test makes sure that forgetting file URLs works
+add_task(async function dontAlwaysForget() {
+ await testForgetAboutThisSite([0], false, []);
+});
+
+// When cancelling the confirmation prompt via ESC key, no entries should be
+// cleared.
+add_task(async function cancelConfirmWithEsc() {
+ await testForgetAboutThisSite([0], false, [], true);
+});
diff --git a/browser/components/places/tests/browser/browser_history_sidebar_search.js b/browser/components/places/tests/browser/browser_history_sidebar_search.js
new file mode 100644
index 0000000000..44d51eec31
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_history_sidebar_search.js
@@ -0,0 +1,71 @@
+add_task(async function test() {
+ let sidebar = document.getElementById("sidebar");
+
+ // Visited pages listed by descending visit date.
+ let pages = [
+ "http://sidebar.mozilla.org/a",
+ "http://sidebar.mozilla.org/b",
+ "http://sidebar.mozilla.org/c",
+ "http://www.mozilla.org/d",
+ ];
+
+ // Number of pages that will be filtered out by the search.
+ const FILTERED_COUNT = 1;
+
+ await PlacesUtils.history.clear();
+
+ // Add some visited page.
+ let time = Date.now();
+ let places = [];
+ for (let i = 0; i < pages.length; i++) {
+ places.push({
+ uri: NetUtil.newURI(pages[i]),
+ visitDate: (time - i) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ });
+ }
+ await PlacesTestUtils.addVisits(places);
+
+ await withSidebarTree("history", function () {
+ info("Set 'by last visited' view");
+ sidebar.contentDocument.getElementById("bylastvisited").doCommand();
+ let tree = sidebar.contentDocument.getElementById("historyTree");
+ check_tree_order(tree, pages);
+
+ // Set a search value.
+ let searchBox = sidebar.contentDocument.getElementById("search-box");
+ ok(searchBox, "search box is in context");
+ searchBox.value = "sidebar.mozilla";
+ searchBox.doCommand();
+ check_tree_order(tree, pages, -FILTERED_COUNT);
+
+ info("Reset the search");
+ searchBox.value = "";
+ searchBox.doCommand();
+ check_tree_order(tree, pages);
+ });
+
+ await PlacesUtils.history.clear();
+});
+
+function check_tree_order(tree, pages, aNumberOfRowsDelta = 0) {
+ let treeView = tree.view;
+ let columns = tree.columns;
+ is(columns.count, 1, "There should be only 1 column in the sidebar");
+
+ let found = 0;
+ for (let i = 0; i < treeView.rowCount; i++) {
+ let node = treeView.nodeForTreeIndex(i);
+ // We could inherit delayed visits from previous tests, skip them.
+ if (!pages.includes(node.uri)) {
+ continue;
+ }
+ is(
+ node.uri,
+ pages[i],
+ "Node is in correct position based on its visit date"
+ );
+ found++;
+ }
+ is(found, pages.length + aNumberOfRowsDelta, "Found all expected results");
+}
diff --git a/browser/components/places/tests/browser/browser_import_button.js b/browser/components/places/tests/browser/browser_import_button.js
new file mode 100644
index 0000000000..146fd746f8
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_import_button.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kPref = "browser.bookmarks.addedImportButton";
+
+/**
+ * Verify that we add the import button only if there aren't enough bookmarks
+ * in the toolbar.
+ */
+add_task(async function test_bookmark_import_button() {
+ let bookmarkCount = (
+ await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid)
+ ).childCount;
+ Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks");
+
+ ok(
+ !document.getElementById("import-button"),
+ "Shouldn't have button to start with."
+ );
+ await PlacesUIUtils.maybeAddImportButton();
+ ok(document.getElementById("import-button"), "Button should be added.");
+ is(Services.prefs.getBoolPref(kPref), true, "Pref should be set.");
+
+ CustomizableUI.reset();
+
+ // Add some bookmarks. This should stop the import button from being inserted.
+ let parentGuid = PlacesUtils.bookmarks.toolbarGuid;
+ let bookmarks = await Promise.all(
+ ["firefox", "rules", "yo"].map(n =>
+ PlacesUtils.bookmarks.insert({
+ parentGuid,
+ url: `https://example.com/${n}`,
+ title: n.toString(),
+ })
+ )
+ );
+
+ // Ensure we remove items after this task, or worst-case after this test
+ // file has completed.
+ let removeAllBookmarks = () => {
+ let removals = bookmarks.map(b => PlacesUtils.bookmarks.remove(b.guid));
+ bookmarks = [];
+ return Promise.all(removals);
+ };
+ registerCleanupFunction(removeAllBookmarks);
+
+ await PlacesUIUtils.maybeAddImportButton();
+ ok(
+ !document.getElementById("import-button"),
+ "Button should not be added if we have bookmarks."
+ );
+
+ // Just in case, for future tests we run:
+ CustomizableUI.reset();
+
+ await removeAllBookmarks();
+});
+
+/**
+ * Verify the button gets removed when we import bookmarks successfully.
+ */
+add_task(async function test_bookmark_import_button_removal() {
+ let bookmarkCount = (
+ await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid)
+ ).childCount;
+ Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks");
+
+ ok(
+ !document.getElementById("import-button"),
+ "Shouldn't have button to start with."
+ );
+ await PlacesUIUtils.maybeAddImportButton();
+ ok(document.getElementById("import-button"), "Button should be added.");
+ is(Services.prefs.getBoolPref(kPref), true, "Pref should be set.");
+
+ PlacesUIUtils.removeImportButtonWhenImportSucceeds();
+
+ Services.obs.notifyObservers(
+ null,
+ "Migration:ItemAfterMigrate",
+ MigrationUtils.resourceTypes.BOOKMARKS
+ );
+
+ is(
+ Services.prefs.getBoolPref(kPref, false),
+ true,
+ "Pref should stay without import."
+ );
+ ok(document.getElementById("import-button"), "Button should still be there.");
+
+ // OK, actually add some bookmarks:
+ MigrationUtils._importQuantities.bookmarks = 5;
+ Services.obs.notifyObservers(
+ null,
+ "Migration:ItemAfterMigrate",
+ MigrationUtils.resourceTypes.BOOKMARKS
+ );
+
+ is(Services.prefs.prefHasUserValue(kPref), false, "Pref should be removed.");
+ ok(
+ !document.getElementById("import-button"),
+ "Button should have been removed."
+ );
+
+ // Reset this, otherwise subsequent tests are going to have a bad time.
+ MigrationUtils._importQuantities.bookmarks = 0;
+});
+
+/**
+ * Check that if the user removes the button, the next startup
+ * we clear the pref and stop monitoring to remove the item.
+ */
+add_task(async function test_bookmark_import_button_removal_cleanup() {
+ let bookmarkCount = (
+ await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid)
+ ).childCount;
+ Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks");
+
+ ok(
+ !document.getElementById("import-button"),
+ "Shouldn't have button to start with."
+ );
+ await PlacesUIUtils.maybeAddImportButton();
+ ok(document.getElementById("import-button"), "Button should be added.");
+ is(Services.prefs.getBoolPref(kPref), true, "Pref should be set.");
+
+ // Simulate the user removing the item.
+ CustomizableUI.removeWidgetFromArea("import-button");
+
+ // We'll call this next startup:
+ PlacesUIUtils.removeImportButtonWhenImportSucceeds();
+
+ // And it should clean up the pref:
+ is(Services.prefs.prefHasUserValue(kPref), false, "Pref should be removed.");
+});
+
+/**
+ * Check that if migration (silently) errors, we still remove the button
+ * _if_ we imported any bookmarks.
+ */
+add_task(async function test_bookmark_import_button_errors() {
+ let bookmarkCount = (
+ await PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.toolbarGuid)
+ ).childCount;
+ Assert.less(bookmarkCount, 3, "we should start with less than 3 bookmarks");
+
+ ok(
+ !document.getElementById("import-button"),
+ "Shouldn't have button to start with."
+ );
+ await PlacesUIUtils.maybeAddImportButton();
+ ok(document.getElementById("import-button"), "Button should be added.");
+ is(Services.prefs.getBoolPref(kPref), true, "Pref should be set.");
+
+ PlacesUIUtils.removeImportButtonWhenImportSucceeds();
+
+ Services.obs.notifyObservers(
+ null,
+ "Migration:ItemError",
+ MigrationUtils.resourceTypes.BOOKMARKS
+ );
+
+ is(
+ Services.prefs.getBoolPref(kPref, false),
+ true,
+ "Pref should stay when fatal error happens."
+ );
+ ok(document.getElementById("import-button"), "Button should still be there.");
+
+ // OK, actually add some bookmarks:
+ MigrationUtils._importQuantities.bookmarks = 5;
+ Services.obs.notifyObservers(
+ null,
+ "Migration:ItemError",
+ MigrationUtils.resourceTypes.BOOKMARKS
+ );
+
+ is(Services.prefs.prefHasUserValue(kPref), false, "Pref should be removed.");
+ ok(
+ !document.getElementById("import-button"),
+ "Button should have been removed."
+ );
+
+ MigrationUtils._importQuantities.bookmarks = 0;
+});
diff --git a/browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js b/browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js
new file mode 100644
index 0000000000..fab8b4c3b0
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/*
+ * Test whether or not that Most Recent Visit of bookmark in library window will
+ * update properly when removing the history.
+ */
+
+const TEST_URLS = ["https://example.com/", "https://example.org/"];
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URLS[0],
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URLS[1],
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URLS[1],
+ });
+ PlacesUtils.tagging.tagURI(makeURI(TEST_URLS[0]), ["test"]);
+ PlacesUtils.tagging.tagURI(makeURI(TEST_URLS[1]), ["test"]);
+
+ registerCleanupFunction(async () => {
+ PlacesUtils.tagging.untagURI(makeURI(TEST_URLS[0]), ["test"]);
+ PlacesUtils.tagging.untagURI(makeURI(TEST_URLS[1]), ["test"]);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function folder() {
+ info("Open bookmarked urls to update most recent visit time");
+ await updateMostRecentVisitTime();
+
+ info("Open library for bookmarks toolbar");
+ const library = await promiseLibrary("BookmarksToolbar");
+
+ info("Add Most Recent Visit column");
+ await showLibraryColumn(library, "placesContentDate");
+
+ info("Check the initial content");
+ const tree = library.document.getElementById("placeContent");
+ Assert.equal(tree.view.rowCount, 3, "All bookmarks are shown");
+ assertRow(tree, 0, TEST_URLS[0], true);
+ assertRow(tree, 1, TEST_URLS[1], true);
+ assertRow(tree, 2, TEST_URLS[1], true);
+
+ info("Clear all visits data");
+ await PlacesUtils.history.remove(TEST_URLS);
+
+ info("Check whether or not the content are updated");
+ assertRow(tree, 0, TEST_URLS[0], false);
+ assertRow(tree, 1, TEST_URLS[1], false);
+ assertRow(tree, 2, TEST_URLS[1], false);
+
+ info("Close library window");
+ await promiseLibraryClosed(library);
+});
+
+add_task(async function tags() {
+ info("Open bookmarked urls to update most recent visit time");
+ await updateMostRecentVisitTime();
+
+ info("Open library for bookmarks toolbar");
+ const library = await promiseLibrary("BookmarksToolbar");
+
+ info("Add Most Recent Visit column");
+ await showLibraryColumn(library, "placesContentDate");
+
+ info("Open test tag");
+ const PO = library.PlacesOrganizer;
+ PO.selectLeftPaneBuiltIn("Tags");
+ const tagsNode = PO._places.selectedNode;
+ PlacesUtils.asContainer(tagsNode).containerOpen = true;
+ const tag = tagsNode.getChild(0);
+ PO._places.selectNode(tag);
+
+ info("Check the initial content");
+ const tree = library.ContentTree.view;
+ Assert.equal(tree.view.rowCount, 3, "All bookmarks are shown");
+ assertRow(tree, 0, TEST_URLS[0], true);
+ assertRow(tree, 1, TEST_URLS[1], true);
+ assertRow(tree, 2, TEST_URLS[1], true);
+
+ info("Clear all visits data");
+ await PlacesUtils.history.remove(TEST_URLS);
+
+ info("Check whether or not the content are updated");
+ assertRow(tree, 0, TEST_URLS[0], false);
+ assertRow(tree, 1, TEST_URLS[1], false);
+ assertRow(tree, 2, TEST_URLS[1], false);
+
+ info("Close library window");
+ await promiseLibraryClosed(library);
+});
+
+async function updateMostRecentVisitTime() {
+ for (const url of TEST_URLS) {
+ const onLoaded = BrowserTestUtils.browserLoaded(gBrowser, false, url);
+ BrowserTestUtils.startLoadingURIString(gBrowser, url);
+ await onLoaded;
+ }
+}
+
+function assertRow(tree, targeRow, expectedUrl, expectMostRecentVisitHasValue) {
+ const url = tree.view.getCellText(targeRow, tree.columns.placesContentUrl);
+ Assert.equal(url, expectedUrl, "URL is correct");
+ const mostRecentVisit = tree.view.getCellText(
+ targeRow,
+ tree.columns.placesContentDate
+ );
+ Assert.equal(
+ !!mostRecentVisit,
+ expectMostRecentVisitHasValue,
+ "Most Recent Visit data is in the cell correctly"
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_library_bookmark_pages.js b/browser/components/places/tests/browser/browser_library_bookmark_pages.js
new file mode 100644
index 0000000000..474adb956d
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_bookmark_pages.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that the a new bookmark is correctly selected after being created via
+ * the bookmark dialog.
+ */
+"use strict";
+
+const TEST_URIS = ["https://example1.com/", "https://example2.com/"];
+let library;
+
+add_setup(async function () {
+ await PlacesTestUtils.addVisits(TEST_URIS);
+
+ library = await promiseLibrary("History");
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_bookmark_page() {
+ library.ContentTree.view.selectPlaceURI(TEST_URIS[0]);
+
+ await withBookmarksDialog(
+ true,
+ async () => {
+ // Open the context menu.
+ let placesContext = library.document.getElementById("placesContext");
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ await promisePopup;
+ let properties = library.document.getElementById(
+ "placesContext_createBookmark"
+ );
+ placesContext.activateItem(properties);
+ },
+ async dialogWin => {
+ Assert.strictEqual(
+ dialogWin.BookmarkPropertiesPanel._itemType,
+ 0,
+ "Should have loaded a bookmark dialog"
+ );
+ Assert.equal(
+ dialogWin.document.getElementById("editBMPanel_locationField").value,
+ TEST_URIS[0],
+ "Should have opened the dialog with the correct uri to be bookmarked"
+ );
+ }
+ );
+});
+
+add_task(async function test_bookmark_pages() {
+ library.ContentTree.view.selectAll();
+
+ await withBookmarksDialog(
+ true,
+ async () => {
+ // Open the context menu.
+ let placesContext = library.document.getElementById("placesContext");
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ await promisePopup;
+ let properties = library.document.getElementById(
+ "placesContext_createBookmark"
+ );
+ placesContext.activateItem(properties);
+ },
+ async dialogWin => {
+ Assert.strictEqual(
+ dialogWin.BookmarkPropertiesPanel._itemType,
+ 1,
+ "Should have loaded a create bookmark folder dialog"
+ );
+ Assert.deepEqual(
+ dialogWin.BookmarkPropertiesPanel._URIs.map(uri => uri.uri.spec),
+ // The list here is reversed, because that's the order they're shown
+ // in the view.
+ [TEST_URIS[1], TEST_URIS[0]],
+ "Should have got the correct URIs for adding to the folder"
+ );
+ }
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js b/browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js
new file mode 100644
index 0000000000..23d3b30564
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Test that a tag can be added to multiple bookmarks at once from the library.
+ */
+"use strict";
+
+const TEST_URLS = ["about:buildconfig", "about:robots"];
+
+add_task(async function test_bulk_tag_from_library() {
+ // Create multiple bookmarks.
+ for (const url of TEST_URLS) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ });
+ }
+
+ // Open library panel.
+ const library = await promiseLibrary("UnfiledBookmarks");
+ const cleanupFn = async () => {
+ await promiseLibraryClosed(library);
+ await PlacesUtils.bookmarks.eraseEverything();
+ };
+ registerCleanupFunction(cleanupFn);
+
+ // Add a tag to multiple bookmarks.
+ library.ContentTree.view.selectAll();
+ const promiseAllTagsChanged = TEST_URLS.map(url =>
+ PlacesTestUtils.waitForNotification("bookmark-tags-changed", events =>
+ events.some(evt => evt.url === url)
+ )
+ );
+ const tag = "some, tag";
+ const tagWithDuplicates = `${tag}, tag`;
+ fillBookmarkTextField("editBMPanel_tagsField", tagWithDuplicates, library);
+ await Promise.all(promiseAllTagsChanged);
+ await TestUtils.waitForCondition(
+ () =>
+ library.document.getElementById("editBMPanel_tagsField").value === tag,
+ "Input field matches the new tags and duplicates are removed."
+ );
+
+ // Verify that the bookmarks were tagged successfully.
+ for (const url of TEST_URLS) {
+ Assert.deepEqual(
+ PlacesUtils.tagging.getTagsForURI(Services.io.newURI(url)),
+ ["some", "tag"],
+ url + " should have the correct tags."
+ );
+ }
+ await cleanupFn();
+});
+
+add_task(async function test_bulk_tag_tags_selector() {
+ // Create multiple bookmarks with a common tag.
+ for (const [i, url] of TEST_URLS.entries()) {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url,
+ });
+ PlacesUtils.tagging.tagURI(Services.io.newURI(url), [
+ "common",
+ `unique_${i}`,
+ ]);
+ }
+
+ // Open library panel.
+ const library = await promiseLibrary("UnfiledBookmarks");
+ const cleanupFn = async () => {
+ await promiseLibraryClosed(library);
+ await PlacesUtils.bookmarks.eraseEverything();
+ };
+ registerCleanupFunction(cleanupFn);
+
+ // Open tags selector.
+ library.document.getElementById("editBMPanel_tagsSelectorRow").hidden = false;
+
+ // Select all bookmarks.
+ const tagsSelector = library.document.getElementById(
+ "editBMPanel_tagsSelector"
+ );
+ library.ContentTree.view.selectAll();
+
+ // Verify that the input field only shows the common tag.
+ await TestUtils.waitForCondition(
+ () =>
+ library.document.getElementById("editBMPanel_tagsField").value ===
+ "common",
+ "Input field only shows the common tag."
+ );
+
+ // Verify that the tags selector shows all tags, and only the common one is
+ // checked.
+ async function checkTagsSelector(aAvailableTags, aCheckedTags) {
+ let tags = await PlacesUtils.bookmarks.fetchTags();
+ is(tags.length, aAvailableTags.length, "Check tags list");
+ let children = tagsSelector.children;
+ is(
+ children.length,
+ aAvailableTags.length,
+ "Found expected number of tags in the tags selector"
+ );
+
+ Array.prototype.forEach.call(children, function (aChild) {
+ let tag = aChild.querySelector("label").getAttribute("value");
+ ok(true, "Found tag '" + tag + "' in the selector");
+ ok(aAvailableTags.includes(tag), "Found expected tag");
+ let checked = aChild.getAttribute("checked") == "true";
+ is(checked, aCheckedTags.includes(tag), "Tag is correctly marked");
+ });
+ }
+
+ async function promiseTagSelectorUpdated(task) {
+ let promise = BrowserTestUtils.waitForEvent(
+ tagsSelector,
+ "BookmarkTagsSelectorUpdated"
+ );
+
+ await task();
+ return promise;
+ }
+
+ info("Check the initial common tag.");
+ await checkTagsSelector(["common", "unique_0", "unique_1"], ["common"]);
+
+ // Verify that the common tag can be edited.
+ await promiseTagSelectorUpdated(() => {
+ info("Edit the common tag.");
+ fillBookmarkTextField("editBMPanel_tagsField", "common_updated", library);
+ });
+ await checkTagsSelector(
+ ["common_updated", "unique_0", "unique_1"],
+ ["common_updated"]
+ );
+
+ // Verify that the common tag can be removed.
+ await promiseTagSelectorUpdated(() => {
+ info("Remove the commmon tag.");
+ fillBookmarkTextField("editBMPanel_tagsField", "", library);
+ });
+ await checkTagsSelector(["unique_0", "unique_1"], []);
+
+ await cleanupFn();
+});
diff --git a/browser/components/places/tests/browser/browser_library_commands.js b/browser/components/places/tests/browser/browser_library_commands.js
new file mode 100644
index 0000000000..254c9503c2
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_commands.js
@@ -0,0 +1,335 @@
+/* -*- 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 enabled commands in the left pane folder of the Library.
+ */
+
+const TEST_URI = NetUtil.newURI("http://www.mozilla.org/");
+
+registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_date_container() {
+ let library = await promiseLibrary();
+ info("Ensure date containers under History cannot be cut but can be deleted");
+
+ await PlacesTestUtils.addVisits(TEST_URI);
+
+ // Select and open the left pane "History" query.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneBuiltIn("History");
+ Assert.notEqual(
+ PO._places.selectedNode,
+ null,
+ "We correctly selected History"
+ );
+
+ // Check that both delete and cut commands are disabled, cause this is
+ // a child of the left pane folder.
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled"
+ );
+ let historyNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ historyNode.containerOpen = true;
+
+ // Check that we have a child container. It is "Today" container.
+ Assert.equal(historyNode.childCount, 1, "History node has one child");
+ let todayNode = historyNode.getChild(0);
+ let todayNodeExpectedTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
+ Assert.equal(
+ todayNode.title,
+ todayNodeExpectedTitle,
+ "History child is the expected container"
+ );
+
+ // Select "Today" container.
+ PO._places.selectNode(todayNode);
+ Assert.equal(
+ PO._places.selectedNode,
+ todayNode,
+ "We correctly selected Today container"
+ );
+ // Check that delete command is enabled but cut command is disabled, cause
+ // this is an history item.
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled"
+ );
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled"
+ );
+
+ // Execute the delete command and check visit has been removed.
+ const promiseURIRemoved = PlacesTestUtils.waitForNotification(
+ "page-removed",
+ events => events[0].url === TEST_URI.spec
+ );
+ PO._places.controller.doCommand("cmd_delete");
+ const removeEvents = await promiseURIRemoved;
+ Assert.ok(
+ removeEvents[0].isRemovedFromStore,
+ "isRemovedFromStore should be true"
+ );
+
+ // Test live update of "History" query.
+ Assert.equal(historyNode.childCount, 0, "History node has no more children");
+
+ historyNode.containerOpen = false;
+
+ Assert.ok(
+ !(await PlacesUtils.history.hasVisits(TEST_URI)),
+ "Visit has been removed"
+ );
+
+ library.close();
+});
+
+add_task(async function test_query_on_toolbar() {
+ let library = await promiseLibrary();
+ info("Ensure queries can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneBuiltIn("BookmarksToolbar");
+ Assert.notEqual(PO._places.selectedNode, null, "We have a valid selection");
+ Assert.equal(
+ PlacesUtils.getConcreteItemGuid(PO._places.selectedNode),
+ PlacesUtils.bookmarks.toolbarGuid,
+ "We have correctly selected bookmarks toolbar node."
+ );
+
+ // Check that both cut and delete commands are disabled, cause this is a child
+ // of the All Bookmarks special query.
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled"
+ );
+
+ let toolbarNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ toolbarNode.containerOpen = true;
+
+ // Add an History query to the toolbar.
+ let query = await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "place:sort=4",
+ title: "special_query",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+
+ // Get first child and check it is the just inserted query.
+ Assert.greater(toolbarNode.childCount, 0, "Toolbar node has children");
+ let queryNode = toolbarNode.getChild(0);
+ Assert.equal(
+ queryNode.title,
+ "special_query",
+ "Query node is correctly selected"
+ );
+
+ // Select query node.
+ PO._places.selectNode(queryNode);
+ Assert.equal(
+ PO._places.selectedNode,
+ queryNode,
+ "We correctly selected query node"
+ );
+
+ // Check that both cut and delete commands are enabled.
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is enabled"
+ );
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled"
+ );
+
+ // Execute the delete command and check bookmark has been removed.
+ let promiseItemRemoved = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => query.guid == event.guid)
+ );
+ PO._places.controller.doCommand("cmd_delete");
+ await promiseItemRemoved;
+
+ Assert.equal(
+ await PlacesUtils.bookmarks.fetch(query.guid),
+ null,
+ "Query node bookmark has been correctly removed"
+ );
+
+ toolbarNode.containerOpen = false;
+
+ library.close();
+});
+
+add_task(async function test_search_contents() {
+ await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "example page",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0,
+ });
+
+ let library = await promiseLibrary();
+ info("Ensure query contents can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneBuiltIn("BookmarksToolbar");
+ Assert.notEqual(PO._places.selectedNode, null, "We have a valid selection");
+ Assert.equal(
+ PlacesUtils.getConcreteItemGuid(PO._places.selectedNode),
+ PlacesUtils.bookmarks.toolbarGuid,
+ "We have correctly selected bookmarks toolbar node."
+ );
+
+ let searchBox = library.document.getElementById("searchFilter");
+ searchBox.value = "example";
+ library.PlacesSearchBox.search(searchBox.value);
+
+ let bookmarkNode = library.ContentTree.view.selectedNode;
+ Assert.equal(
+ bookmarkNode.uri,
+ "http://example.com/",
+ "Found the expected bookmark"
+ );
+
+ // Check that both cut and delete commands are enabled.
+ Assert.ok(
+ library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is enabled"
+ );
+ Assert.ok(
+ library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled"
+ );
+
+ library.close();
+});
+
+add_task(async function test_tags() {
+ await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://example.com/",
+ title: "example page",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0,
+ });
+ PlacesUtils.tagging.tagURI(NetUtil.newURI("http://example.com/"), ["test"]);
+
+ let library = await promiseLibrary();
+ info("Ensure query contents can be cut or deleted");
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneBuiltIn("Tags");
+ let tagsNode = PO._places.selectedNode;
+ Assert.notEqual(tagsNode, null, "We have a valid selection");
+ let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
+ Assert.equal(tagsNode.title, tagsTitle, "Tags has been properly selected");
+
+ // Check that both cut and delete commands are disabled.
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is disabled"
+ );
+
+ // Now select the tag.
+ PlacesUtils.asContainer(tagsNode).containerOpen = true;
+ let tag = tagsNode.getChild(0);
+ PO._places.selectNode(tag);
+ Assert.equal(
+ PO._places.selectedNode.title,
+ "test",
+ "The created tag has been properly selected"
+ );
+
+ // Check that cut is disabled but delete is enabled.
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ !PO._places.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled"
+ );
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled"
+ );
+
+ let bookmarkNode = library.ContentTree.view.selectedNode;
+ Assert.equal(
+ bookmarkNode.uri,
+ "http://example.com/",
+ "Found the expected bookmark"
+ );
+
+ // Check that both cut and delete commands are enabled.
+ Assert.ok(
+ library.ContentTree.view.controller.isCommandEnabled("cmd_copy"),
+ "Copy command is enabled"
+ );
+ Assert.ok(
+ !library.ContentTree.view.controller.isCommandEnabled("cmd_cut"),
+ "Cut command is disabled"
+ );
+ Assert.ok(
+ library.ContentTree.view.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled"
+ );
+
+ tagsNode.containerOpen = false;
+
+ library.close();
+});
diff --git a/browser/components/places/tests/browser/browser_library_delete.js b/browser/components/places/tests/browser/browser_library_delete.js
new file mode 100644
index 0000000000..fe95be0604
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_delete.js
@@ -0,0 +1,157 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that Library correctly handles deletes.
+ */
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+const TEST_URL = "http://www.batch.delete.me/";
+
+var gLibrary;
+
+add_task(async function test_setup() {
+ gLibrary = await promiseLibrary();
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ // Close Library window.
+ gLibrary.close();
+ });
+});
+
+add_task(async function test_create_and_remove_bookmarks() {
+ let bmChildren = [];
+ for (let i = 0; i < 10; i++) {
+ bmChildren.push({
+ title: `bm${i}`,
+ url: TEST_URL,
+ });
+ }
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "deleteme",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: bmChildren,
+ },
+ {
+ title: "keepme",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ ],
+ });
+
+ // Select and open the left pane "History" query.
+ let PO = gLibrary.PlacesOrganizer;
+ PO.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ Assert.notEqual(PO._places.selectedNode, null, "Selected unsorted bookmarks");
+
+ let unsortedNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ Assert.equal(unsortedNode.childCount, 2, "Unsorted node has 2 children");
+ let folderNode = unsortedNode.getChild(0);
+ Assert.equal(
+ folderNode.title,
+ "deleteme",
+ "Folder found in unsorted bookmarks"
+ );
+
+ // Check delete command is available.
+ PO._places.selectNode(folderNode);
+ Assert.equal(
+ PO._places.selectedNode.title,
+ "deleteme",
+ "Folder node selected"
+ );
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled"
+ );
+ let promiseItemRemovedNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => event.guid == folderNode.bookmarkGuid)
+ );
+
+ // Press the delete key and check that the bookmark has been removed.
+ gLibrary.document.getElementById("placesList").focus();
+ EventUtils.synthesizeKey("VK_DELETE", {}, gLibrary);
+
+ await promiseItemRemovedNotification;
+
+ Assert.ok(
+ !(await PlacesUtils.bookmarks.fetch({ url: TEST_URL })),
+ "Bookmark has been correctly removed"
+ );
+ // Test live update.
+ Assert.equal(unsortedNode.childCount, 1, "Unsorted node has 1 child");
+ Assert.equal(PO._places.selectedNode.title, "keepme", "Folder node selected");
+});
+
+add_task(async function test_ensure_correct_selection_and_functionality() {
+ let PO = gLibrary.PlacesOrganizer;
+ let ContentTree = gLibrary.ContentTree;
+ // Move selection forth and back.
+ PO.selectLeftPaneBuiltIn("History");
+ PO.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ // Now select the "keepme" folder in the right pane and delete it.
+ ContentTree.view.selectNode(ContentTree.view.result.root.getChild(0));
+ Assert.equal(
+ ContentTree.view.selectedNode.title,
+ "keepme",
+ "Found folder in content pane"
+ );
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "bm",
+ url: TEST_URL,
+ });
+
+ Assert.equal(
+ ContentTree.view.result.root.childCount,
+ 2,
+ "Right pane was correctly updated"
+ );
+});
+
+add_task(async function test_repeated_remove_bookmark() {
+ // Select and open the left pane "History" query.
+ let PO = gLibrary.PlacesOrganizer;
+ PO.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ gLibrary.document.getElementById("placesList").focus();
+ let unsortedNode = PlacesUtils.asContainer(PO._places.selectedNode);
+ Assert.equal(unsortedNode.childCount, 1, "Unsorted node has 1 child");
+ let folderNode = unsortedNode.getChild(0);
+ Assert.equal(
+ folderNode.title,
+ "keepme",
+ "Folder found in unsorted bookmarks"
+ );
+ PO._places.selectNode(folderNode);
+
+ Assert.ok(
+ PO._places.controller.isCommandEnabled("cmd_delete"),
+ "Delete command is enabled"
+ );
+ registerCleanupFunction(sinon.restore);
+ let spy = sinon.spy(PO._places.controller, "remove");
+ let stub = sinon
+ .stub(PO._places.controller, "_removeRowsFromBookmarks")
+ .resolves();
+ PO._places.controller.doCommand("cmd_delete");
+ PO._places.controller.doCommand("cmd_delete");
+ PO._places.controller.doCommand("cmd_delete");
+ Assert.equal(spy.callCount, 3, "Should have been invoked thrice");
+ Assert.equal(stub.callCount, 1, "Should have been invoked once");
+ // Executing another command allows delete to go through again.
+ PO._places.controller.doCommand("cmd_cut");
+ PO._places.controller.doCommand("cmd_delete");
+ Assert.equal(spy.callCount, 4, "Should have been invoked again");
+ Assert.equal(stub.callCount, 2, "Should have been invoked again");
+});
diff --git a/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js b/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js
new file mode 100644
index 0000000000..ed124a047a
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js
@@ -0,0 +1,111 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test deleting bookmarks from within tags.
+ */
+
+registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_tags() {
+ const uris = [
+ Services.io.newURI("http://example.com/1"),
+ Services.io.newURI("http://example.com/2"),
+ Services.io.newURI("http://example.com/3"),
+ ];
+
+ let children = uris.map((uri, index, arr) => {
+ return {
+ title: `bm${index}`,
+ url: uri,
+ };
+ });
+
+ // Note: we insert the uris in reverse order, so that we end up with the
+ // display in "logical" order of bm0 at the top, and bm2 at the bottom.
+ children = children.reverse();
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children,
+ });
+
+ for (let uri of uris) {
+ PlacesUtils.tagging.tagURI(uri, ["test"]);
+ }
+
+ let library = await promiseLibrary();
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneBuiltIn("Tags");
+ let tagsNode = PO._places.selectedNode;
+ Assert.notEqual(tagsNode, null, "Should have a valid selection");
+ let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
+ Assert.equal(tagsNode.title, tagsTitle, "Should have selected the Tags node");
+
+ // Now select the tag.
+ PlacesUtils.asContainer(tagsNode).containerOpen = true;
+ let tag = tagsNode.getChild(0);
+ PO._places.selectNode(tag);
+ Assert.equal(
+ PO._places.selectedNode.title,
+ "test",
+ "Should have selected the created tag"
+ );
+
+ let ContentTree = library.ContentTree;
+
+ for (let i = 0; i < uris.length; i++) {
+ ContentTree.view.selectNode(ContentTree.view.result.root.getChild(0));
+
+ Assert.equal(
+ ContentTree.view.selectedNode.title,
+ `bm${i}`,
+ `Should have selected bm${i}`
+ );
+
+ let promiseNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-tags-changed"
+ );
+
+ ContentTree.view.controller.doCommand("cmd_delete");
+
+ await promiseNotification;
+
+ for (let j = 0; j < uris.length; j++) {
+ let tags = PlacesUtils.tagging.getTagsForURI(uris[j]);
+ if (j <= i) {
+ Assert.equal(
+ tags.length,
+ 0,
+ `There should be no tags for the URI: ${uris[j].spec}`
+ );
+ } else {
+ Assert.equal(
+ tags.length,
+ 1,
+ `There should be one tag for the URI: ${uris[j].spec}`
+ );
+ }
+ }
+ }
+
+ // The tag should now not exist.
+ Assert.equal(
+ await PlacesUtils.bookmarks.fetch({ tags: ["test"] }),
+ null,
+ "There should be no URIs remaining for the tag"
+ );
+
+ tagsNode.containerOpen = false;
+
+ library.close();
+});
diff --git a/browser/components/places/tests/browser/browser_library_delete_tags.js b/browser/components/places/tests/browser/browser_library_delete_tags.js
new file mode 100644
index 0000000000..d5f3eafc63
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_delete_tags.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 enabled commands in the left pane folder of the Library.
+ */
+
+registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+add_task(async function test_tags() {
+ const TEST_URI = Services.io.newURI("http://example.com/");
+
+ await PlacesUtils.bookmarks.insert({
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: TEST_URI,
+ title: "example page",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: 0,
+ });
+ PlacesUtils.tagging.tagURI(TEST_URI, ["test"]);
+
+ let library = await promiseLibrary();
+
+ // Select and open the left pane "Bookmarks Toolbar" folder.
+ let PO = library.PlacesOrganizer;
+
+ PO.selectLeftPaneBuiltIn("Tags");
+ let tagsNode = PO._places.selectedNode;
+ Assert.notEqual(tagsNode, null, "Should have a valid selection");
+ let tagsTitle = PlacesUtils.getString("TagsFolderTitle");
+ Assert.equal(tagsNode.title, tagsTitle, "Should have selected the Tags node");
+
+ // Now select the tag.
+ PlacesUtils.asContainer(tagsNode).containerOpen = true;
+ let tag = tagsNode.getChild(0);
+ PO._places.selectNode(tag);
+ Assert.equal(
+ PO._places.selectedNode.title,
+ "test",
+ "Should have selected the created tag"
+ );
+
+ PO._places.controller.doCommand("cmd_delete");
+
+ await PlacesTestUtils.promiseAsyncUpdates();
+
+ let tags = PlacesUtils.tagging.getTagsForURI(TEST_URI);
+
+ Assert.equal(tags.length, 0, "There should be no tags for the URI");
+
+ tagsNode.containerOpen = false;
+
+ library.close();
+});
diff --git a/browser/components/places/tests/browser/browser_library_downloads.js b/browser/components/places/tests/browser/browser_library_downloads.js
new file mode 100644
index 0000000000..3aa37b1fed
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_downloads.js
@@ -0,0 +1,65 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Tests bug 564900: Add folder specifically for downloads to Library left pane.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=564900
+ * This test visits various pages then opens the Library and ensures
+ * that both the Downloads folder shows up and that the correct visits
+ * are shown in it.
+ */
+
+add_task(async function test() {
+ // Add visits.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://mozilla.org",
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ },
+ {
+ uri: "http://google.com",
+ transition: PlacesUtils.history.TRANSITION_DOWNLOAD,
+ },
+ {
+ uri: "http://en.wikipedia.org",
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ },
+ {
+ uri: "http://ubuntu.org",
+ transition: PlacesUtils.history.TRANSITION_DOWNLOAD,
+ },
+ ]);
+
+ let library = await promiseLibrary("Downloads");
+
+ registerCleanupFunction(async () => {
+ await library.close();
+ await PlacesUtils.history.clear();
+ });
+
+ // Make sure Downloads is present.
+ Assert.notEqual(
+ library.PlacesOrganizer._places.selectedNode,
+ null,
+ "Downloads is present and selected"
+ );
+
+ // Check results.
+ let testURIs = ["http://ubuntu.org/", "http://google.com/"];
+
+ await TestUtils.waitForCondition(
+ () =>
+ library.ContentArea.currentView.associatedElement.itemChildren.length ==
+ testURIs.length
+ );
+
+ for (let element of library.ContentArea.currentView.associatedElement
+ .itemChildren) {
+ Assert.equal(
+ element._shell.download.source.url,
+ testURIs.shift(),
+ "URI matches"
+ );
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_library_left_pane_middleclick.js b/browser/components/places/tests/browser/browser_library_left_pane_middleclick.js
new file mode 100644
index 0000000000..fc33963199
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_left_pane_middleclick.js
@@ -0,0 +1,106 @@
+/* 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 middle-clicking items in the Library.
+ */
+
+const URIs = ["about:license", "about:mozilla"];
+
+var gLibrary = null;
+
+add_task(async function test_setup() {
+ // Temporary disable history, so we won't record pages navigation.
+ await SpecialPowers.pushPrefEnv({ set: [["places.history.enabled", false]] });
+
+ // Open Library window.
+ gLibrary = await promiseLibrary();
+
+ registerCleanupFunction(async () => {
+ // We must close "Other Bookmarks" ready for other tests.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = false;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Close Library window.
+ await promiseLibraryClosed(gLibrary);
+ });
+});
+
+add_task(async function test_open_folder_in_tabs() {
+ let children = URIs.map(url => {
+ return {
+ title: "Title",
+ url,
+ };
+ });
+
+ // Create a new folder.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "Folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children,
+ },
+ ],
+ });
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ Assert.notEqual(
+ gLibrary.PlacesOrganizer._places.selectedNode,
+ null,
+ "We correctly have selection in the Library left pane"
+ );
+
+ // Get our bookmark in the right pane.
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ Assert.equal(folderNode.title, "Folder", "Found folder in the right pane");
+
+ gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = true;
+
+ // Now middle-click on the bookmark contained with it.
+ let promiseLoaded = Promise.all(
+ URIs.map(uri => BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true))
+ );
+
+ let bookmarkedNode =
+ gLibrary.PlacesOrganizer._places.selectedNode.getChild(0);
+ mouseEventOnCell(
+ gLibrary.PlacesOrganizer._places,
+ gLibrary.PlacesOrganizer._places.view.treeIndexForNode(bookmarkedNode),
+ 0,
+ { button: 1 }
+ );
+
+ let tabs = await promiseLoaded;
+
+ Assert.ok(true, "Expected tabs were loaded");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) {
+ var selection = aTree.view.selection;
+ selection.select(aRowIndex);
+ aTree.ensureRowIsVisible(aRowIndex);
+ var column = aTree.columns[aColumnIndex];
+
+ // get cell coordinates
+ var rect = aTree.getCoordsForCellItem(aRowIndex, column, "text");
+
+ EventUtils.synthesizeMouse(
+ aTree.body,
+ rect.x,
+ rect.y,
+ aEventDetails,
+ gLibrary
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js
new file mode 100644
index 0000000000..6caaf5a9d4
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_task(async function () {
+ let hierarchy = ["AllBookmarks", "BookmarksMenu"];
+
+ let items = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: [
+ {
+ title: "Folder 1",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "Folder 2",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "Bookmark",
+ url: "http://example.com",
+ },
+ ],
+ },
+ ],
+ },
+ ],
+ });
+
+ hierarchy.push(items[0].guid, items[1].guid);
+
+ let library = await promiseLibrary();
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(items[0]);
+ await promiseLibraryClosed(library);
+ });
+
+ library.PlacesOrganizer.selectLeftPaneContainerByHierarchy(hierarchy);
+
+ Assert.equal(
+ library.PlacesOrganizer._places.selectedNode.bookmarkGuid,
+ items[1].guid,
+ "Found the expected left pane selected node"
+ );
+
+ Assert.equal(
+ library.ContentTree.view.view.nodeForTreeIndex(0).bookmarkGuid,
+ items[2].guid,
+ "Found the expected right pane contents"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_library_middleclick.js b/browser/components/places/tests/browser/browser_library_middleclick.js
new file mode 100644
index 0000000000..8eb0bfa008
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_middleclick.js
@@ -0,0 +1,234 @@
+/* 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 middle-clicking items in the Library.
+ */
+
+const URIs = ["about:license", "about:mozilla"];
+
+var gLibrary = null;
+var gTests = [];
+
+add_task(async function test_setup() {
+ // Increase timeout, this test can be quite slow due to waitForFocus calls.
+ requestLongerTimeout(2);
+
+ // Temporary disable history, so we won't record pages navigation.
+ await SpecialPowers.pushPrefEnv({ set: [["places.history.enabled", false]] });
+
+ // Ensure the database is empty.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Open Library window.
+ gLibrary = await promiseLibrary();
+
+ registerCleanupFunction(async () => {
+ // We must close "Other Bookmarks" ready for other tests.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = false;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Close Library window.
+ await promiseLibraryClosed(gLibrary);
+ });
+});
+
+gTests.push({
+ desc: "Open bookmark in a new tab.",
+ URIs: ["about:buildconfig"],
+ _bookmark: null,
+
+ async setup() {
+ // Add a new unsorted bookmark.
+ this._bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Title",
+ url: this.URIs[0],
+ });
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ Assert.notEqual(
+ gLibrary.PlacesOrganizer._places.selectedNode,
+ null,
+ "We correctly have selection in the Library left pane"
+ );
+
+ // Get our bookmark in the right pane.
+ var bookmarkNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ Assert.equal(
+ bookmarkNode.uri,
+ this.URIs[0],
+ "Found bookmark in the right pane"
+ );
+ },
+
+ async cleanup() {
+ await PlacesUtils.bookmarks.remove(this._bookmark);
+ },
+});
+
+// ------------------------------------------------------------------------------
+// Open a folder in tabs.
+//
+gTests.push({
+ desc: "Open a folder in tabs.",
+ URIs: ["about:buildconfig", "about:mozilla"],
+ _bookmarks: null,
+
+ async setup() {
+ // Create a new folder.
+ let children = this.URIs.map(url => {
+ return {
+ title: "Title",
+ url,
+ };
+ });
+
+ this._bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "Folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children,
+ },
+ ],
+ });
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ isnot(
+ gLibrary.PlacesOrganizer._places.selectedNode,
+ null,
+ "We correctly have selection in the Library left pane"
+ );
+ // Get our bookmark in the right pane.
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.title, "Folder", "Found folder in the right pane");
+ },
+
+ async cleanup() {
+ await PlacesUtils.bookmarks.remove(this._bookmarks[0]);
+ },
+});
+
+// ------------------------------------------------------------------------------
+// Open a query in tabs.
+
+gTests.push({
+ desc: "Open a query in tabs.",
+ URIs: ["about:buildconfig", "about:mozilla"],
+ _bookmarks: null,
+ _query: null,
+
+ async setup() {
+ let children = this.URIs.map(url => {
+ return {
+ title: "Title",
+ url,
+ };
+ });
+
+ this._bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "Folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children,
+ },
+ ],
+ });
+
+ // Create a bookmarks query containing our bookmarks.
+ var hs = PlacesUtils.history;
+ var options = hs.getNewQueryOptions();
+ options.queryType = Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS;
+ var query = hs.getNewQuery();
+ // The colon included in the terms selects only about: URIs. If not included
+ // we also may get pages like about.html included in the query result.
+ query.searchTerms = "about:";
+ var queryString = hs.queryToQueryString(query, options);
+ this._query = await PlacesUtils.bookmarks.insert({
+ index: 0, // it must be the first
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "Query",
+ url: queryString,
+ });
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ isnot(
+ gLibrary.PlacesOrganizer._places.selectedNode,
+ null,
+ "We correctly have selection in the Library left pane"
+ );
+ // Get our bookmark in the right pane.
+ var folderNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ is(folderNode.title, "Query", "Found query in the right pane");
+ },
+
+ async cleanup() {
+ await PlacesUtils.bookmarks.remove(this._bookmarks[0]);
+ await PlacesUtils.bookmarks.remove(this._query);
+ },
+});
+
+async function runTest(test) {
+ info("Start of test: " + test.desc);
+ // Test setup will set Library so that the bookmark to be opened is the
+ // first node in the content (right pane) tree.
+ await test.setup();
+
+ // Middle click on first node in the content tree of the Library.
+ gLibrary.focus();
+ await SimpleTest.promiseFocus(gLibrary);
+
+ // Now middle-click on the bookmark contained with it.
+ let promiseLoaded = Promise.all(
+ test.URIs.map(uri =>
+ BrowserTestUtils.waitForNewTab(gBrowser, uri, false, true)
+ )
+ );
+
+ mouseEventOnCell(gLibrary.ContentTree.view, 0, 0, { button: 1 });
+
+ let tabs = await promiseLoaded;
+
+ Assert.ok(true, "Expected tabs were loaded");
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+
+ await test.cleanup();
+}
+
+add_task(async function test_all() {
+ for (let test of gTests) {
+ await runTest(test);
+ }
+});
+
+function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) {
+ var selection = aTree.view.selection;
+ selection.select(aRowIndex);
+ aTree.ensureRowIsVisible(aRowIndex);
+ var column = aTree.columns[aColumnIndex];
+
+ // get cell coordinates
+ var rect = aTree.getCoordsForCellItem(aRowIndex, column, "text");
+
+ EventUtils.synthesizeMouse(
+ aTree.body,
+ rect.x,
+ rect.y,
+ aEventDetails,
+ gLibrary
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_library_new_bookmark.js b/browser/components/places/tests/browser/browser_library_new_bookmark.js
new file mode 100644
index 0000000000..dff7accc44
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_new_bookmark.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that the a new bookmark is correctly selected after being created via
+ * the bookmark dialog.
+ */
+"use strict";
+
+let bookmarks = [
+ {
+ url: "https://example1.com",
+ title: "bm1",
+ },
+ {
+ url: "https://example2.com",
+ title: "bm2",
+ },
+ {
+ url: "https://example3.com",
+ title: "bm3",
+ },
+];
+
+add_task(async function test_open_bookmark_from_library() {
+ let bm = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: bookmarks,
+ });
+
+ let library = await promiseLibrary("UnfiledBookmarks");
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ let bmLibrary = library.ContentTree.view.view.nodeForTreeIndex(1);
+ Assert.equal(
+ bmLibrary.title,
+ bm[1].title,
+ "EditBookmark: Found bookmark in the right pane"
+ );
+
+ library.ContentTree.view.selectNode(bmLibrary);
+
+ let beforeUpdatedPRTime;
+ await withBookmarksDialog(
+ false,
+ async () => {
+ // Open the context menu.
+ let placesContext = library.document.getElementById("placesContext");
+ let promisePopup = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ await promisePopup;
+ let properties = library.document.getElementById(
+ "placesContext_new:bookmark"
+ );
+ placesContext.activateItem(properties, {});
+ },
+ async dialogWin => {
+ beforeUpdatedPRTime = Date.now() * 1000;
+
+ fillBookmarkTextField(
+ "editBMPanel_locationField",
+ "https://example4.com/",
+ dialogWin,
+ false
+ );
+
+ EventUtils.synthesizeKey("VK_RETURN", {}, dialogWin);
+ }
+ );
+ let node = library.ContentTree.view.selectedNode;
+ Assert.ok(node, "EditBookmark: Should have a selectedNode");
+ Assert.equal(
+ node.uri,
+ "https://example4.com/",
+ "EditBookmark: Should have selected the newly created bookmark"
+ );
+ Assert.greater(
+ node.lastModified,
+ beforeUpdatedPRTime,
+ "EditBookmark: The lastModified should be greater than the time of before updating"
+ );
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/places/tests/browser/browser_library_openFlatContainer.js b/browser/components/places/tests/browser/browser_library_openFlatContainer.js
new file mode 100644
index 0000000000..1d9cc61f2f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_openFlatContainer.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test opening a flat container in the right pane even if its parent in the
+ * left pane is closed.
+ */
+
+var library;
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ await promiseLibraryClosed(library);
+ });
+});
+
+add_task(async function test_open_built_in_folder() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ title: "testBM",
+ url: "http://example.com/1",
+ });
+
+ library = await promiseLibrary("AllBookmarks");
+
+ library.ContentTree.view.selectItems([PlacesUtils.bookmarks.menuGuid]);
+
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ clickCount: 2,
+ });
+
+ Assert.equal(
+ library.PlacesOrganizer._places.selectedNode.bookmarkGuid,
+ PlacesUtils.bookmarks.virtualMenuGuid,
+ "Should have the bookmarks menu selected in the left pane."
+ );
+ Assert.equal(
+ library.ContentTree.view.view.nodeForTreeIndex(0).bookmarkGuid,
+ bm.guid,
+ "Should have the expected bookmark selected in the right pane"
+ );
+});
+
+add_task(async function test_open_new_folder_in_unfiled() {
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "Folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "Bookmark",
+ url: "http://example.com",
+ },
+ ],
+ },
+ ],
+ });
+
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ // Ensure the container is closed.
+ library.PlacesOrganizer._places.selectedNode.containerOpen = false;
+
+ let folderNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ Assert.equal(
+ folderNode.bookmarkGuid,
+ bookmarks[0].guid,
+ "Found the expected folder in the right pane"
+ );
+ // Select the folder node in the right pane.
+ library.ContentTree.view.selectNode(folderNode);
+
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ clickCount: 2,
+ });
+
+ Assert.equal(
+ library.ContentTree.view.view.nodeForTreeIndex(0).bookmarkGuid,
+ bookmarks[1].guid,
+ "Found the expected bookmark in the right pane"
+ );
+});
+
+add_task(async function test_open_history_query() {
+ const todayTitle = PlacesUtils.getString("finduri-AgeInDays-is-0");
+ await PlacesTestUtils.addVisits([
+ {
+ uri: "http://example.com",
+ title: "Whittingtons",
+ },
+ ]);
+
+ library.PlacesOrganizer.selectLeftPaneBuiltIn("History");
+
+ // Ensure the container is closed.
+ library.PlacesOrganizer._places.selectedNode.containerOpen = false;
+
+ let query = library.ContentTree.view.view.nodeForTreeIndex(0);
+ Assert.equal(query.title, todayTitle, "Should have the today query");
+
+ library.ContentTree.view.selectNode(query);
+
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ clickCount: 2,
+ });
+
+ Assert.equal(
+ library.PlacesOrganizer._places.selectedNode.title,
+ todayTitle,
+ "Should have selected the today query in the left-pane."
+ );
+ Assert.equal(
+ library.ContentTree.view.view.nodeForTreeIndex(0).title,
+ "Whittingtons",
+ "Found the expected history item in the right pane"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_library_open_all.js b/browser/components/places/tests/browser/browser_library_open_all.js
new file mode 100644
index 0000000000..18f0014936
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_open_all.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ const TEST_EXAMPLE_URL = "http://example.com/";
+ const TEST_EXAMPLE_PARAMS = "?foo=1|2";
+ const TEST_EXAMPLE_TITLE = "Example Domain";
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_EXAMPLE_URL + TEST_EXAMPLE_PARAMS,
+ title: TEST_EXAMPLE_TITLE,
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_EXAMPLE_URL,
+ title: TEST_EXAMPLE_TITLE,
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_open_all_in_tabs_from_library() {
+ let gLibrary = await promiseLibrary("AllBookmarks");
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+ gLibrary.ContentTree.view.selectAll();
+ let placesContext = gLibrary.document.getElementById("placesContext");
+ let promiseContextMenu = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await promiseContextMenu;
+ let openTabs = gLibrary.document.getElementById(
+ "placesContext_openBookmarkLinks:tabs"
+ );
+ let promiseWaitForWindow = BrowserTestUtils.waitForNewWindow();
+ placesContext.activateItem(openTabs, { shiftKey: true });
+ let newWindow = await promiseWaitForWindow;
+
+ Assert.equal(
+ newWindow.browserDOMWindow.tabCount,
+ 2,
+ "Expected number of tabs opened in new window"
+ );
+
+ await BrowserTestUtils.closeWindow(newWindow);
+ await promiseLibraryClosed(gLibrary);
+});
diff --git a/browser/components/places/tests/browser/browser_library_open_all_with_separator.js b/browser/components/places/tests/browser/browser_library_open_all_with_separator.js
new file mode 100644
index 0000000000..a68158a1ba
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_open_all_with_separator.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ title: "Example One",
+ url: "https://example.com/1/",
+ },
+ {
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ },
+ {
+ title: "Example Two",
+ url: "https://example.com/2/",
+ },
+ ],
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_open_all_without_separator() {
+ let gLibrary = await promiseLibrary("AllBookmarks");
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+ gLibrary.ContentTree.view.selectAll();
+
+ let placesContext = gLibrary.document.getElementById("placesContext");
+ let promiseContextMenu = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await promiseContextMenu;
+
+ let openTabs = gLibrary.document.getElementById(
+ "placesContext_openBookmarkLinks:tabs"
+ );
+ let promiseWaitForWindow = BrowserTestUtils.waitForNewWindow();
+ placesContext.activateItem(openTabs, { shiftKey: true });
+ let newWindow = await promiseWaitForWindow;
+
+ Assert.equal(
+ newWindow.browserDOMWindow.tabCount,
+ 2,
+ "Expected number of tabs opened in new window"
+ );
+
+ await BrowserTestUtils.closeWindow(newWindow);
+ await promiseLibraryClosed(gLibrary);
+});
diff --git a/browser/components/places/tests/browser/browser_library_open_bookmark.js b/browser/components/places/tests/browser/browser_library_open_bookmark.js
new file mode 100644
index 0000000000..7532d7c1c9
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_open_bookmark.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test that the a bookmark can be opened from the Library by mouse double click.
+ */
+"use strict";
+
+const TEST_URL = "about:buildconfig";
+
+add_task(async function test_open_bookmark_from_library() {
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: TEST_URL,
+ });
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ let gLibrary = await promiseLibrary("UnfiledBookmarks");
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(gLibrary);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await BrowserTestUtils.removeTab(tab);
+ });
+
+ let bmLibrary = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ Assert.equal(bmLibrary.title, bm.title, "Found bookmark in the right pane");
+
+ gLibrary.ContentTree.view.selectNode(bmLibrary);
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ clickCount: 2,
+ });
+
+ await BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ TEST_URL
+ );
+ Assert.ok(true, "Expected tab was loaded");
+});
diff --git a/browser/components/places/tests/browser/browser_library_open_leak.js b/browser/components/places/tests/browser/browser_library_open_leak.js
new file mode 100644
index 0000000000..02a0874c76
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_open_leak.js
@@ -0,0 +1,22 @@
+/* -*- 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 474831
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=474831
+ *
+ * Tests for leaks caused by simply opening and closing the Places Library
+ * window. Opens the Places Library window, waits for it to load, closes it,
+ * and finishes.
+ */
+
+add_task(async function test_open_and_close() {
+ let library = await promiseLibrary();
+
+ Assert.ok(true, "Library has been correctly opened");
+
+ await promiseLibraryClosed(library);
+});
diff --git a/browser/components/places/tests/browser/browser_library_panel_leak.js b/browser/components/places/tests/browser/browser_library_panel_leak.js
new file mode 100644
index 0000000000..f8b536cecc
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_panel_leak.js
@@ -0,0 +1,71 @@
+/* -*- 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 433231 - Places Library leaks the nsGlobalWindow when closed with a
+ * history entry selected.
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=433231
+ *
+ * STRs: Open Library, select an history entry in History, close Library.
+ * ISSUE: We were adding a bookmarks observer when editing a bookmark, when
+ * selecting an history entry the panel was not un-initialized, and
+ * since an history entry does not have an itemId, the observer was
+ * never removed.
+ */
+
+const TEST_URI = "http://www.mozilla.org/";
+
+add_task(async function test_no_leak_closing_library_with_history_selected() {
+ // Add an history entry.
+ await PlacesTestUtils.addVisits(TEST_URI);
+
+ let organizer = await promiseLibrary();
+
+ let contentTree = organizer.document.getElementById("placeContent");
+ Assert.notEqual(
+ contentTree,
+ null,
+ "Sanity check: placeContent tree should exist"
+ );
+ Assert.notEqual(
+ organizer.PlacesOrganizer,
+ null,
+ "Sanity check: PlacesOrganizer should exist"
+ );
+ Assert.notEqual(
+ organizer.gEditItemOverlay,
+ null,
+ "Sanity check: gEditItemOverlay should exist"
+ );
+
+ Assert.ok(
+ organizer.gEditItemOverlay.initialized,
+ "gEditItemOverlay is initialized"
+ );
+ Assert.notEqual(
+ organizer.gEditItemOverlay._paneInfo.itemGuid,
+ "",
+ "Editing a bookmark"
+ );
+
+ // Select History in the left pane.
+ organizer.PlacesOrganizer.selectLeftPaneBuiltIn("History");
+ // Select the first history entry.
+ let selection = contentTree.view.selection;
+ selection.clearSelection();
+ selection.rangedSelect(0, 0, true);
+ // Check the panel is editing the history entry.
+ Assert.equal(
+ organizer.gEditItemOverlay._paneInfo.itemGuid,
+ "",
+ "Editing an history entry"
+ );
+ // Close Library window.
+ organizer.close();
+
+ // Clean up history.
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/places/tests/browser/browser_library_sameNodeDetailsPaneOptimization.js b/browser/components/places/tests/browser/browser_library_sameNodeDetailsPaneOptimization.js
new file mode 100644
index 0000000000..d9b800697d
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_sameNodeDetailsPaneOptimization.js
@@ -0,0 +1,45 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const sandbox = sinon.createSandbox();
+
+/**
+ * This checks an optimization in the Library code, that tries to not update
+ * the details pane if the same node that is being edited is picked again.
+ * That happens for example if the focus moves to the details pane and back
+ * to the tree.
+ */
+
+add_task(async function () {
+ let bm1 = await PlacesUtils.bookmarks.insert({
+ url: "https://bookmark1.mozilla.org/",
+ title: "Bookmark 1",
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+
+ let library = await promiseLibrary("UnfiledBookmarks");
+ registerCleanupFunction(async () => {
+ sandbox.restore();
+ await promiseLibraryClosed(library);
+ await PlacesUtils.bookmarks.remove(bm1);
+ });
+
+ let nameField = library.document.getElementById("editBMPanel_namePicker");
+ let tree = library.ContentTree.view;
+ tree.selectItems([bm1.guid]);
+ Assert.equal(tree.selectedNode.title, "Bookmark 1");
+ await synthesizeClickOnSelectedTreeCell(tree);
+ Assert.equal(nameField.value, "Bookmark 1");
+
+ let updateSpy = sandbox.spy(library.PlacesOrganizer, "updateDetailsPane");
+ let uninitSpy = sandbox.spy(library.gEditItemOverlay, "uninitPanel");
+ nameField.focus();
+ await synthesizeClickOnSelectedTreeCell(tree);
+ Assert.ok(updateSpy.calledOnce, "should try to update the details pane");
+ Assert.ok(uninitSpy.notCalled, "should skip the update cause same node");
+});
diff --git a/browser/components/places/tests/browser/browser_library_search.js b/browser/components/places/tests/browser/browser_library_search.js
new file mode 100644
index 0000000000..898f664269
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_search.js
@@ -0,0 +1,206 @@
+/* -*- 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 451151
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=451151
+ *
+ * Summary:
+ * Tests frontend Places Library searching -- search, search reset, search scope
+ * consistency.
+ *
+ * Details:
+ * Each test below
+ * 1. selects a folder in the left pane and ensures that the content tree is
+ * appropriately updated,
+ * 2. performs a search and ensures that the content tree is correct for the
+ * folder and search and that the search UI is visible and appropriate to
+ * folder,
+ * 5. resets the search and ensures that the content tree is correct and that
+ * the search UI is hidden, and
+ * 6. if folder scope was clicked, searches again and ensures folder scope
+ * remains selected.
+ */
+
+const TEST_URL = "http://dummy.mozilla.org/";
+const TEST_DOWNLOAD_URL = "http://dummy.mozilla.org/dummy.pdf";
+const TEST_PARENT_FOLDER = "testParentFolder";
+const TEST_SIF_URL = "http://testsif.example.com/";
+const TEST_SIF_TITLE = "TestSIF";
+
+var gLibrary;
+
+/**
+ * Performs a search for a given folder and search string and ensures that the
+ * URI of the right pane's content tree is as expected for the folder and search
+ * string. Also ensures that the search scope button is as expected after the
+ * search.
+ *
+ * @param {string} aFolderGuid
+ * the item guid of a node in the left pane's tree
+ * @param {string} aSearchStr
+ * the search text; may be empty to reset the search
+ */
+async function search(aFolderGuid, aSearchStr) {
+ let doc = gLibrary.document;
+ let folderTree = doc.getElementById("placesList");
+ let contentTree = doc.getElementById("placeContent");
+
+ // First, ensure that selecting the folder in the left pane updates the
+ // content tree properly.
+ if (aFolderGuid) {
+ folderTree.selectItems([aFolderGuid]);
+ Assert.notEqual(
+ folderTree.selectedNode,
+ null,
+ "Sanity check: left pane tree should have selection after selecting!"
+ );
+
+ // The downloads folder never quite matches the url of the contentTree,
+ // probably due to the way downloads are loaded.
+ if (aFolderGuid !== PlacesUtils.virtualDownloadsGuid) {
+ Assert.equal(
+ folderTree.selectedNode.uri,
+ contentTree.place,
+ "Content tree's folder should be what was selected in the left pane"
+ );
+ }
+ }
+
+ // Second, ensure that searching updates the content tree and search UI
+ // properly.
+ let searchBox = doc.getElementById("searchFilter");
+ searchBox.value = aSearchStr;
+ gLibrary.PlacesSearchBox.search(searchBox.value);
+ let query = {};
+ PlacesUtils.history.queryStringToQuery(
+ contentTree.result.root.uri,
+ query,
+ {}
+ );
+ if (aSearchStr) {
+ Assert.equal(
+ query.value.searchTerms,
+ aSearchStr,
+ "Content tree's searchTerms should be text in search box"
+ );
+ } else {
+ Assert.equal(
+ query.value.hasSearchTerms,
+ false,
+ "Content tree's searchTerms should not exist after search reset"
+ );
+ }
+}
+
+async function showInFolder(aFolderGuid, aSearchStr, aParentFolderGuid) {
+ let doc = gLibrary.document;
+ let folderTree = doc.getElementById("placesList");
+ let contentTree = doc.getElementById("placeContent");
+
+ let searchBox = doc.getElementById("searchFilter");
+ searchBox.value = aSearchStr;
+ gLibrary.PlacesSearchBox.search(searchBox.value);
+ let theNode = contentTree.view._getNodeForRow(0);
+ let bookmarkGuid = theNode.bookmarkGuid;
+
+ Assert.equal(theNode.uri, TEST_SIF_URL, "Found expected bookmark");
+
+ contentTree.selectNode(theNode);
+ info("Executing showInFolder");
+ info("Waiting for showInFolder to select folder in tree");
+ let folderSelected = BrowserTestUtils.waitForEvent(folderTree, "select");
+ contentTree.controller.doCommand("placesCmd_showInFolder");
+ await folderSelected;
+
+ let treeNode = folderTree.selectedNode;
+ let contentNode = contentTree.selectedNode;
+ Assert.equal(
+ treeNode.bookmarkGuid,
+ aParentFolderGuid,
+ "Containing folder node selected on left tree pane"
+ );
+ Assert.equal(
+ contentNode.bookmarkGuid,
+ bookmarkGuid,
+ "The searched bookmark guid matches selected node in content pane"
+ );
+ Assert.equal(
+ contentNode.uri,
+ TEST_SIF_URL,
+ "The searched bookmark URL matches selected node in content pane"
+ );
+}
+
+add_task(async function test() {
+ // Add visits, a bookmark and a tag.
+ await PlacesTestUtils.addVisits([
+ {
+ uri: Services.io.newURI(TEST_URL),
+ visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ },
+ {
+ uri: Services.io.newURI(TEST_DOWNLOAD_URL),
+ visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_DOWNLOAD,
+ },
+ ]);
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "dummy",
+ url: TEST_URL,
+ });
+
+ PlacesUtils.tagging.tagURI(Services.io.newURI(TEST_URL), ["dummyTag"]);
+
+ gLibrary = await promiseLibrary();
+
+ const rootsToTest = [
+ PlacesUtils.virtualAllBookmarksGuid,
+ PlacesUtils.virtualHistoryGuid,
+ PlacesUtils.virtualDownloadsGuid,
+ ];
+
+ for (let root of rootsToTest) {
+ await search(root, "dummy");
+ }
+
+ await promiseLibraryClosed(gLibrary);
+
+ // Cleanup before testing Show in Folder.
+ PlacesUtils.tagging.untagURI(Services.io.newURI(TEST_URL), ["dummyTag"]);
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+
+ // Now test Show in Folder
+ gLibrary = await promiseLibrary();
+ info("Test Show in Folder");
+ let parentFolder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: TEST_PARENT_FOLDER,
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: parentFolder.guid,
+ title: TEST_SIF_TITLE,
+ url: TEST_SIF_URL,
+ });
+
+ await showInFolder(
+ PlacesUtils.virtualAllBookmarksGuid,
+ TEST_SIF_TITLE,
+ parentFolder.guid
+ );
+
+ // Cleanup
+ await promiseLibraryClosed(gLibrary);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
diff --git a/browser/components/places/tests/browser/browser_library_tags_visibility.js b/browser/components/places/tests/browser/browser_library_tags_visibility.js
new file mode 100644
index 0000000000..d0427ef591
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_tags_visibility.js
@@ -0,0 +1,88 @@
+/* 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 whether tags are shown in library window.
+ */
+
+registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+const TEST_URI = Services.io.newURI("https://example.com/");
+const TEST_TAGS = ["tagB", "tagA"];
+const TAGS_TEXT = TEST_TAGS.sort().join(", ");
+
+add_task(async function base() {
+ const { guid } = await PlacesUtils.bookmarks.insert({
+ url: TEST_URI,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ await PlacesTestUtils.addVisits(TEST_URI);
+ PlacesUtils.tagging.tagURI(TEST_URI, TEST_TAGS);
+
+ const library = await promiseLibrary();
+ registerCleanupFunction(() => {
+ library.close();
+ });
+
+ const {
+ document: libraryDocument,
+ PlacesOrganizer: placesOrganizer,
+ ContentTree: contentTree,
+ } = library;
+
+ info("Test in Bookmarks");
+ placesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ contentTree.view.selectItems([guid]);
+ await assertTagsVisibility(libraryDocument, true);
+
+ info("Test in Tags");
+ placesOrganizer.selectLeftPaneBuiltIn("Tags");
+ const tagsContainer = PlacesUtils.asContainer(
+ placesOrganizer._places.selectedNode
+ );
+ tagsContainer.containerOpen = true;
+ const targetNode = tagsContainer.getChild(0);
+ placesOrganizer._places.selectNode(targetNode);
+ contentTree.view.selectItems([guid]);
+ await assertTagsVisibility(libraryDocument, true);
+
+ info("Test in History");
+ placesOrganizer.selectLeftPaneBuiltIn("History");
+ const historyNode = PlacesUtils.asContainer(
+ placesOrganizer._places.selectedNode
+ );
+ historyNode.containerOpen = true;
+ const todayNode = historyNode.getChild(0);
+ placesOrganizer._places.selectNode(todayNode);
+ contentTree.view.selectItems([guid]);
+ await assertTagsVisibility(libraryDocument, false);
+});
+
+async function assertTagsVisibility(libraryDocument, expectedVisible) {
+ const locationInput = libraryDocument.getElementById(
+ "editBMPanel_locationField"
+ );
+ await BrowserTestUtils.waitForCondition(
+ () => locationInput.value === TEST_URI.spec,
+ "Wait until the panel ready"
+ );
+
+ // Check the editor area.
+ const tagInput = libraryDocument.getElementById("editBMPanel_tagsField");
+ Assert.equal(BrowserTestUtils.isVisible(tagInput), expectedVisible);
+ if (expectedVisible) {
+ Assert.equal(tagInput.value, TAGS_TEXT);
+ }
+
+ // Check the cell.
+ const tree = libraryDocument.getElementById("placeContent");
+ const expectedCellText = expectedVisible ? TAGS_TEXT : null;
+ Assert.equal(
+ tree.view.getCellText(0, tree.columns.placesContentTags),
+ expectedCellText
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_library_telemetry.js b/browser/components/places/tests/browser/browser_library_telemetry.js
new file mode 100644
index 0000000000..00ca4635d8
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_telemetry.js
@@ -0,0 +1,413 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+// Visited pages listed by descending visit date.
+const pages = [
+ "https://library.mozilla.org/a",
+ "https://library.mozilla.org/b",
+ "https://library.mozilla.org/c",
+ "https://www.mozilla.org/d",
+];
+
+// The prompt returns 1 for cancelled and 0 for accepted.
+let gResponse = 1;
+(function replacePromptService() {
+ let originalPromptService = Services.prompt;
+ Services.prompt = {
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]),
+ confirmEx: () => gResponse,
+ };
+ registerCleanupFunction(() => {
+ Services.prompt = originalPromptService;
+ });
+})();
+
+async function searchHistory(gLibrary, searchTerm) {
+ let doc = gLibrary.document;
+ let contentTree = doc.getElementById("placeContent");
+
+ let searchBox = doc.getElementById("searchFilter");
+ searchBox.value = searchTerm;
+ gLibrary.PlacesSearchBox.search(searchBox.value);
+ let query = {};
+ PlacesUtils.history.queryStringToQuery(
+ contentTree.result.root.uri,
+ query,
+ {}
+ );
+ Assert.equal(
+ query.value.searchTerms,
+ searchTerm,
+ "Content tree's searchTerms should be text in search box"
+ );
+}
+
+function searchBookmarks(gLibrary, searchTerm) {
+ let searchBox = gLibrary.document.getElementById("searchFilter");
+ searchBox.value = searchTerm;
+ gLibrary.PlacesSearchBox.search(searchBox.value);
+}
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add some visited pages to history
+ let time = Date.now();
+ let places = [];
+ for (let i = 0; i < pages.length; i++) {
+ places.push({
+ uri: NetUtil.newURI(pages[i]),
+ visitDate: (time - i) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ });
+ }
+ await PlacesTestUtils.addVisits(places);
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "Mozilla",
+ url: "https://www.mozilla.org/",
+ },
+ {
+ title: "Example",
+ url: "https://sidebar.mozilla.org/",
+ },
+ ],
+ });
+ await registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_library_history_telemetry() {
+ Services.telemetry.clearScalars();
+ let cumulativeSearchesHistogram = Services.telemetry.getHistogramById(
+ "PLACES_LIBRARY_CUMULATIVE_HISTORY_SEARCHES"
+ );
+
+ let gLibrary = await promiseLibrary("History");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.opened",
+ "history",
+ 1
+ );
+
+ let currentSelectedLeftPaneNode =
+ gLibrary.PlacesOrganizer._places.selectedNode;
+ if (
+ currentSelectedLeftPaneNode.title == "History" &&
+ currentSelectedLeftPaneNode.hasChildren &&
+ currentSelectedLeftPaneNode.getChild(0).title == "Today"
+ ) {
+ // Select "Today" node under History if not already selected
+ gLibrary.PlacesOrganizer._places.selectNode(
+ currentSelectedLeftPaneNode.getChild(0)
+ );
+ }
+
+ Assert.equal(
+ gLibrary.PlacesOrganizer._places.selectedNode.title,
+ "Today",
+ "The Today history sidebar button is selected"
+ );
+
+ await searchHistory(gLibrary, "mozilla");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.search",
+ "history",
+ 1
+ );
+
+ let firstHistoryNode = gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ Assert.equal(
+ firstHistoryNode.uri,
+ pages[0],
+ "Found history item in the right pane"
+ );
+
+ // Double click first History link to open it
+ gLibrary.ContentTree.view.selectNode(firstHistoryNode);
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ clickCount: 2,
+ });
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "history",
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 1, 1);
+ info("Cumulative search telemetry looks right");
+
+ cumulativeSearchesHistogram.clear();
+
+ // Close and reopen Libary window
+ await promiseLibraryClosed(gLibrary);
+ gLibrary = await promiseLibrary("History");
+
+ Assert.equal(
+ gLibrary.PlacesOrganizer._places.selectedNode.title,
+ "Today",
+ "The Today history sidebar button is selected"
+ );
+
+ // if a user tries to open all history entries and they cancel opening
+ // those entries in new tabs (due to max tab limit warning),
+ // no telemetry should be recorded
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.maxOpenBeforeWarn", 4]],
+ });
+
+ // Reject opening all tabs when prompted
+ gResponse = 1;
+
+ // Open all history entries
+ synthesizeClickOnSelectedTreeCell(gLibrary.PlacesOrganizer._places, {
+ button: 1,
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link"
+ );
+
+ // Make 4 searches before opening History links
+ await searchHistory(gLibrary, "library a", 1);
+ info("First search was performed.");
+ await searchHistory(gLibrary, "library b", 2);
+ info("Second search was performed.");
+ await searchHistory(gLibrary, "library c", 3);
+ info("Third search was performed.");
+ await searchHistory(gLibrary, "mozilla", 4);
+ info("Fourth search was performed.");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.search",
+ "history",
+ 4
+ );
+
+ // Accept opening all tabs when prompted
+ gResponse = 0;
+
+ // Open all history entries
+ synthesizeClickOnSelectedTreeCell(gLibrary.PlacesOrganizer._places, {
+ button: 1,
+ });
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "history",
+ 4
+ );
+
+ TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 4, 1);
+ info("Cumulative search telemetry looks right");
+
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link"
+ );
+
+ let openOption = document.getElementById("placesContext_open");
+ openOption.click();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "history",
+ 1
+ );
+
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link"
+ );
+
+ let openNewTabOption = document.getElementById("placesContext_open:newtab");
+ openNewTabOption.click();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "history",
+ 1
+ );
+
+ let newWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link"
+ );
+
+ let openNewWindowOption = document.getElementById(
+ "placesContext_open:newwindow"
+ );
+ openNewWindowOption.click();
+
+ let newWin = await newWinOpened;
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "history",
+ 1
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+
+ let newPrivateWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link"
+ );
+
+ let openNewPrivateWindowOption = document.getElementById(
+ "placesContext_open:newprivatewindow"
+ );
+ openNewPrivateWindowOption.click();
+
+ let newPrivateWin = await newPrivateWinOpened;
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "history",
+ 1
+ );
+
+ await BrowserTestUtils.closeWindow(newPrivateWin);
+
+ cumulativeSearchesHistogram.clear();
+ await promiseLibraryClosed(gLibrary);
+ await SpecialPowers.popPrefEnv();
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function test_library_bookmarks_telemetry() {
+ Services.telemetry.clearScalars();
+ let cumulativeSearchesHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "PLACES_LIBRARY_CUMULATIVE_BOOKMARK_SEARCHES"
+ );
+
+ let library = await promiseLibrary("AllBookmarks");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.opened",
+ "bookmarks",
+ 1
+ );
+
+ searchBookmarks(library, "mozilla");
+
+ // reset
+ searchBookmarks(library, "");
+
+ // search again
+ searchBookmarks(library, "moz");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.search",
+ "bookmarks",
+ 2
+ );
+
+ let firstNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ library.ContentTree.view.selectNode(firstNode);
+
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ clickCount: 2,
+ });
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "bookmarks",
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 2, 1);
+
+ cumulativeSearchesHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "PLACES_LIBRARY_CUMULATIVE_BOOKMARK_SEARCHES"
+ );
+
+ // do another search to make sure everything has been cleared
+ searchBookmarks(library, "moz");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.search",
+ "bookmarks",
+ 1
+ );
+
+ firstNode = library.ContentTree.view.view.nodeForTreeIndex(0);
+ library.ContentTree.view.selectNode(firstNode);
+
+ synthesizeClickOnSelectedTreeCell(library.ContentTree.view, {
+ clickCount: 2,
+ });
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "library.link",
+ "bookmarks",
+ 1
+ );
+
+ TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 1, 1);
+
+ cumulativeSearchesHistogram.clear();
+ await promiseLibraryClosed(library);
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_library_tree_leak.js b/browser/components/places/tests/browser/browser_library_tree_leak.js
new file mode 100644
index 0000000000..9e552f5c31
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_tree_leak.js
@@ -0,0 +1,24 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+
+"use strict";
+
+add_task(async function bookmark_leak_window() {
+ // A library window has two trees after selecting a bookmark item:
+ // A left tree (#placesList) and a right tree (#placeContent).
+ // Upon closing the window, both trees are destructed, in an unspecified
+ // order. In bug 1520047, a memory leak was observed when the left tree
+ // was destroyed last.
+
+ let library = await promiseLibrary("BookmarksToolbar");
+ let tree = library.document.getElementById("placesList");
+ tree.selectItems(["toolbar_____"]);
+
+ await synthesizeClickOnSelectedTreeCell(tree);
+ await promiseLibraryClosed(library);
+
+ Assert.ok(
+ true,
+ "Closing a window after selecting a node in the tree should not cause a leak"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_library_views_liveupdate.js b/browser/components/places/tests/browser/browser_library_views_liveupdate.js
new file mode 100644
index 0000000000..ca79768f8f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_views_liveupdate.js
@@ -0,0 +1,217 @@
+/* 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 Library Left pane view for liveupdate.
+ */
+
+let gLibrary = null;
+
+add_setup(async function () {
+ gLibrary = await promiseLibrary();
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ await promiseLibraryClosed(gLibrary);
+ });
+});
+
+async function testInFolder(folderGuid, prefix) {
+ let addedBookmarks = [];
+
+ let item = await insertAndCheckItem(
+ {
+ parentGuid: folderGuid,
+ title: `${prefix}1`,
+ url: `http://${prefix}1.mozilla.org/`,
+ },
+ 0
+ );
+ item.title = `${prefix}1_edited`;
+ await updateAndCheckItem(item, 0);
+ addedBookmarks.push(item);
+
+ item = await insertAndCheckItem(
+ {
+ parentGuid: folderGuid,
+ title: `${prefix}2`,
+ url: "place:",
+ },
+ 0
+ );
+
+ item.title = `${prefix}2_edited`;
+ await updateAndCheckItem(item, 0);
+ addedBookmarks.push(item);
+
+ item = await insertAndCheckItem(
+ {
+ parentGuid: folderGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ },
+ 2
+ );
+ addedBookmarks.push(item);
+
+ item = await insertAndCheckItem(
+ {
+ parentGuid: folderGuid,
+ title: `${prefix}f`,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ 1
+ );
+
+ item.title = `${prefix}f_edited`;
+ await updateAndCheckItem(item, 1);
+
+ item.index = 0;
+ await updateAndCheckItem(item, 0);
+ addedBookmarks.push(item);
+
+ let folderGuid1 = item.guid;
+
+ item = await insertAndCheckItem(
+ {
+ parentGuid: folderGuid1,
+ title: `${prefix}f1`,
+ url: `http://${prefix}f1.mozilla.org/`,
+ },
+ 0
+ );
+ addedBookmarks.push(item);
+
+ item = await insertAndCheckItem(
+ {
+ parentGuid: folderGuid,
+ title: `${prefix}f12`,
+ url: `http://${prefix}f12.mozilla.org/`,
+ },
+ 4
+ );
+ addedBookmarks.push(item);
+
+ // Move to a different folder and index.
+ item.parentGuid = folderGuid1;
+ item.index = 0;
+ await updateAndCheckItem(item, 0);
+
+ return addedBookmarks;
+}
+
+add_task(async function test() {
+ let addedBookmarks = [];
+
+ info("*** Acting on menu bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.menuGuid, "bm")
+ );
+
+ info("*** Acting on toolbar bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.toolbarGuid, "tb")
+ );
+
+ info("*** Acting on unsorted bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.unfiledGuid, "ub")
+ );
+
+ // Remove bookmarks in reverse order, so that the effects are correct.
+ for (let i = addedBookmarks.length - 1; i >= 0; i--) {
+ await removeAndCheckItem(addedBookmarks[i]);
+ }
+});
+
+function selectItem(item, parentTreeIndex) {
+ let useLeftPane =
+ item.type == PlacesUtils.bookmarks.TYPE_FOLDER || // is Folder
+ (item.type == PlacesUtils.bookmarks.TYPE_BOOKMARK &&
+ item.url.protocol == "place:"); // is Query
+ let tree = useLeftPane
+ ? gLibrary.PlacesOrganizer._places
+ : gLibrary.ContentTree.view;
+ tree.selectItems([item.guid]);
+ let treeIndex = tree.view.treeIndexForNode(tree.selectedNode);
+ let title = tree.view.getCellText(treeIndex, tree.columns.getColumnAt(0));
+ if (useLeftPane) {
+ // Make the treeIndex relative to the parent, otherwise getting the right
+ // index value is tricky due to separators and URIs being hidden in the left
+ // pane.
+ treeIndex -= parentTreeIndex + 1;
+ }
+ return {
+ node: tree.selectedNode,
+ title,
+ parentRelativeTreeIndex: treeIndex,
+ };
+}
+
+function itemExists(item) {
+ let useLeftPane =
+ item.type == PlacesUtils.bookmarks.TYPE_FOLDER || // is Folder
+ (item.type == PlacesUtils.bookmarks.TYPE_BOOKMARK &&
+ item.url.protocol == "place:"); // is Query
+ let tree = useLeftPane
+ ? gLibrary.PlacesOrganizer._places
+ : gLibrary.ContentTree.view;
+ tree.selectItems([item.guid], true);
+ return tree.selectedNode?.bookmarkGuid == item.guid;
+}
+
+async function insertAndCheckItem(insertItem, expectedParentRelativeIndex) {
+ // Ensure the parent is selected before the change, this covers live updating
+ // better than selecting the parent later, that would just refresh all its
+ // children.
+ Assert.ok(insertItem.parentGuid, "Must have a parentGuid");
+ gLibrary.PlacesOrganizer._places.selectItems([insertItem.parentGuid], true);
+ let tree = gLibrary.PlacesOrganizer._places;
+ let parentTreeIndex = tree.view.treeIndexForNode(tree.selectedNode);
+
+ let item = await PlacesUtils.bookmarks.insert(insertItem);
+
+ let { node, title, parentRelativeTreeIndex } = selectItem(
+ item,
+ parentTreeIndex
+ );
+ Assert.equal(item.guid, node.bookmarkGuid, "Should find the updated node");
+ Assert.equal(title, item.title, "Should have the correct title");
+ Assert.equal(
+ parentRelativeTreeIndex,
+ expectedParentRelativeIndex,
+ "Should have the expected index"
+ );
+ return item;
+}
+
+async function updateAndCheckItem(updateItem, expectedParentRelativeTreeIndex) {
+ // Ensure the parent is selected before the change, this covers live updating
+ // better than selecting the parent later, that would just refresh all its
+ // children.
+ Assert.ok(updateItem.parentGuid, "Must have a parentGuid");
+ gLibrary.PlacesOrganizer._places.selectItems([updateItem.parentGuid], true);
+ let tree = gLibrary.PlacesOrganizer._places;
+ let parentTreeIndex = tree.view.treeIndexForNode(tree.selectedNode);
+
+ let item = await PlacesUtils.bookmarks.update(updateItem);
+
+ let { node, title, parentRelativeTreeIndex } = selectItem(
+ item,
+ parentTreeIndex
+ );
+ Assert.equal(item.guid, node.bookmarkGuid, "Should find the updated node");
+ Assert.equal(title, item.title, "Should have the correct title");
+ Assert.equal(
+ parentRelativeTreeIndex,
+ expectedParentRelativeTreeIndex,
+ "Should have the expected index"
+ );
+ return item;
+}
+
+async function removeAndCheckItem(itemData) {
+ await PlacesUtils.bookmarks.remove(itemData);
+ Assert.ok(!itemExists(itemData), "Should not find the updated node");
+}
diff --git a/browser/components/places/tests/browser/browser_library_warnOnOpen.js b/browser/components/places/tests/browser/browser_library_warnOnOpen.js
new file mode 100644
index 0000000000..4289d414df
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_library_warnOnOpen.js
@@ -0,0 +1,159 @@
+/* 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 1435562 - Test that browser.tabs.warnOnOpen is respected when
+ * opening multiple items from the Library. */
+
+"use strict";
+
+var gLibrary = null;
+
+add_setup(async function () {
+ // Temporarily disable history, so we won't record pages navigation.
+ await SpecialPowers.pushPrefEnv({ set: [["places.history.enabled", false]] });
+
+ // Open Library window.
+ gLibrary = await promiseLibrary();
+
+ registerCleanupFunction(async () => {
+ // We must close "Other Bookmarks" ready for other tests.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = false;
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Close Library window.
+ await promiseLibraryClosed(gLibrary);
+ });
+});
+
+add_task(async function test_warnOnOpenFolder() {
+ // Generate a list of links larger than browser.tabs.maxOpenBeforeWarn
+ const MAX_LINKS = 16;
+ let children = [];
+ for (let i = 0; i < MAX_LINKS; i++) {
+ children.push({
+ title: `Folder Target ${i}`,
+ url: `http://example${i}.com`,
+ });
+ }
+
+ // Create a new folder containing our links.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "bigFolder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children,
+ },
+ ],
+ });
+ info("Pushed test folder into the bookmarks tree");
+
+ // Select unsorted bookmarks root in the left pane.
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+ info("Got selection in the Library left pane");
+
+ // Get our bookmark in the right pane.
+ gLibrary.ContentTree.view.view.nodeForTreeIndex(0);
+ info("Got bigFolder in the right pane");
+
+ gLibrary.PlacesOrganizer._places.selectedNode.containerOpen = true;
+
+ // Middle-click on folder (opens all links in folder) and then cancel opening in the dialog
+ let promiseLoaded = BrowserTestUtils.promiseAlertDialog("cancel");
+ let bookmarkedNode =
+ gLibrary.PlacesOrganizer._places.selectedNode.getChild(0);
+ mouseEventOnCell(
+ gLibrary.PlacesOrganizer._places,
+ gLibrary.PlacesOrganizer._places.view.treeIndexForNode(bookmarkedNode),
+ 0,
+ { button: 1 }
+ );
+
+ await promiseLoaded;
+
+ Assert.ok(
+ true,
+ "Expected dialog was shown when attempting to open folder with lots of links"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_warnOnOpenLinks() {
+ // Generate a list of links larger than browser.tabs.maxOpenBeforeWarn
+ const MAX_LINKS = 16;
+ let children = [];
+ for (let i = 0; i < MAX_LINKS; i++) {
+ children.push({
+ title: `Highlighted Target ${i}`,
+ url: `http://example${i}.com`,
+ });
+ }
+
+ // Insert the links into the tree
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children,
+ });
+ info("Pushed test folder into the bookmarks tree");
+
+ gLibrary.PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+ info("Got selection in the Library left pane");
+
+ // Select all the links
+ gLibrary.ContentTree.view.selectAll();
+
+ let placesContext = gLibrary.document.getElementById("placesContext");
+ let promiseContextMenu = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+
+ // Open up the context menu and select "Open All In Tabs" (the first item in the list)
+ synthesizeClickOnSelectedTreeCell(gLibrary.ContentTree.view, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ await promiseContextMenu;
+ info("Context menu opened as expected");
+
+ let openTabs = gLibrary.document.getElementById(
+ "placesContext_openBookmarkLinks:tabs"
+ );
+ let promiseLoaded = BrowserTestUtils.promiseAlertDialog("cancel");
+
+ placesContext.activateItem(openTabs, {});
+
+ await promiseLoaded;
+
+ Assert.ok(
+ true,
+ "Expected dialog was shown when attempting to open lots of selected links"
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+function mouseEventOnCell(aTree, aRowIndex, aColumnIndex, aEventDetails) {
+ var selection = aTree.view.selection;
+ selection.select(aRowIndex);
+ aTree.ensureRowIsVisible(aRowIndex);
+ var column = aTree.columns[aColumnIndex];
+
+ // get cell coordinates
+ var rect = aTree.getCoordsForCellItem(aRowIndex, column, "text");
+
+ EventUtils.synthesizeMouse(
+ aTree.body,
+ rect.x,
+ rect.y,
+ aEventDetails,
+ gLibrary
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
new file mode 100644
index 0000000000..2daf822db3
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_markPageAsFollowedLink.js
@@ -0,0 +1,75 @@
+/**
+ * Tests that visits across frames are correctly represented in the database.
+ */
+
+const BASE_URL =
+ "http://mochi.test:8888/browser/browser/components/places/tests/browser";
+const PAGE_URL = BASE_URL + "/framedPage.html";
+const LEFT_URL = BASE_URL + "/frameLeft.html";
+const RIGHT_URL = BASE_URL + "/frameRight.html";
+
+add_task(async function test() {
+ // We must wait for both frames to be loaded and the visits to be registered.
+ let deferredLeftFrameVisit = Promise.withResolvers();
+ let deferredRightFrameVisit = Promise.withResolvers();
+
+ Services.obs.addObserver(function observe(subject) {
+ (async function () {
+ let url = subject.QueryInterface(Ci.nsIURI).spec;
+ if (url == LEFT_URL) {
+ is(
+ await getTransitionForUrl(url),
+ null,
+ "Embed visits should not get a database entry."
+ );
+ deferredLeftFrameVisit.resolve();
+ } else if (url == RIGHT_URL) {
+ is(
+ await getTransitionForUrl(url),
+ PlacesUtils.history.TRANSITION_FRAMED_LINK,
+ "User activated visits should get a FRAMED_LINK transition."
+ );
+ Services.obs.removeObserver(observe, "uri-visit-saved");
+ deferredRightFrameVisit.resolve();
+ }
+ })();
+ }, "uri-visit-saved");
+
+ // Open a tab and wait for all the subframes to load.
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE_URL);
+
+ // Wait for the left frame visit to be registered.
+ info("Waiting left frame visit");
+ await deferredLeftFrameVisit.promise;
+
+ // Click on the link in the left frame to cause a page load in the
+ // right frame.
+ info("Clicking link");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function () {
+ content.frames[0].document.getElementById("clickme").click();
+ });
+
+ // Wait for the right frame visit to be registered.
+ info("Waiting right frame visit");
+ await deferredRightFrameVisit.promise;
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+function getTransitionForUrl(url) {
+ return PlacesUtils.withConnectionWrapper(
+ "browser_markPageAsFollowedLink",
+ async db => {
+ let rows = await db.execute(
+ `
+ SELECT visit_type
+ FROM moz_historyvisits
+ JOIN moz_places h ON place_id = h.id
+ WHERE url_hash = hash(:url) AND url = :url
+ `,
+ { url }
+ );
+ return rows.length ? rows[0].getResultByName("visit_type") : null;
+ }
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js b/browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js
new file mode 100644
index 0000000000..3a5527a689
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+const TEST_URL = "https://www.example.com/";
+
+/**
+ * Checks that the Bookmarks subview is updated after deleting an item.
+ */
+add_task(async function test_panelview_bookmarks_delete() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_URL,
+ title: TEST_URL,
+ });
+
+ await gCUITestUtils.openMainMenu();
+
+ document.getElementById("appMenu-bookmarks-button").click();
+ let bookmarksView = document.getElementById("PanelUI-bookmarks");
+ let promise = BrowserTestUtils.waitForEvent(bookmarksView, "ViewShown");
+ await promise;
+
+ let list = document.getElementById("panelMenu_bookmarksMenu");
+ let listItem = [...list.children].find(node => node.label == TEST_URL);
+
+ let placesContext = document.getElementById("placesContext");
+ promise = BrowserTestUtils.waitForEvent(placesContext, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(listItem, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await promise;
+
+ promise = new Promise(resolve => {
+ let observer = new MutationObserver(mutations => {
+ if (listItem.parentNode == null) {
+ Assert.ok(true, "The bookmarks list item was removed.");
+ observer.disconnect();
+ resolve();
+ }
+ });
+ observer.observe(list, { childList: true });
+ });
+ let placesContextDelete = document.getElementById(
+ "placesContext_deleteBookmark"
+ );
+ placesContext.activateItem(placesContextDelete, {});
+ await promise;
+
+ await gCUITestUtils.hideMainMenu();
+});
diff --git a/browser/components/places/tests/browser/browser_paste_bookmarks.js b/browser/components/places/tests/browser/browser_paste_bookmarks.js
new file mode 100644
index 0000000000..8a23db4cdc
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_paste_bookmarks.js
@@ -0,0 +1,439 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "http://example.com/";
+const TEST_URL1 = "https://example.com/otherbrowser/";
+
+var PlacesOrganizer;
+var ContentTree;
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ let organizer = await promiseLibrary();
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(organizer);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ PlacesOrganizer = organizer.PlacesOrganizer;
+ ContentTree = organizer.ContentTree;
+
+ // Show date added column.
+ await showLibraryColumn(organizer, "placesContentDateAdded");
+});
+
+add_task(async function paste() {
+ info("Selecting BookmarksToolbar in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+
+ let dateAdded = new Date();
+ dateAdded.setHours(10);
+ dateAdded.setMinutes(10);
+ dateAdded.setSeconds(0);
+
+ let bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_URL,
+ title: "0",
+ dateAdded,
+ });
+
+ ContentTree.view.selectItems([bookmark.guid]);
+
+ await promiseClipboard(() => {
+ info("Cutting selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ let tree = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+
+ Assert.equal(
+ tree.children.length,
+ 1,
+ "Should be one bookmark in the unfiled folder."
+ );
+ Assert.equal(tree.children[0].title, "0", "Should have the correct title");
+ Assert.equal(tree.children[0].uri, TEST_URL, "Should have the correct URL");
+ Assert.equal(
+ tree.children[0].dateAdded,
+ PlacesUtils.toPRTime(dateAdded),
+ "Should have the correct date"
+ );
+
+ Assert.ok(
+ ContentTree.view.view
+ .getCellText(0, ContentTree.view.columns.placesContentDateAdded)
+ .startsWith(`${dateAdded.getHours()}:${dateAdded.getMinutes()}`),
+ "Should reflect the data added"
+ );
+
+ await PlacesUtils.bookmarks.remove(tree.children[0].guid);
+});
+
+add_task(async function paste_check_indexes() {
+ info("Selecting BookmarksToolbar in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+
+ let copyChildren = [];
+ let targetChildren = [];
+ for (let i = 0; i < 10; i++) {
+ copyChildren.push({
+ url: `${TEST_URL}${i}`,
+ title: `Copy ${i}`,
+ });
+ targetChildren.push({
+ url: `${TEST_URL1}${i}`,
+ title: `Target ${i}`,
+ });
+ }
+
+ let copyBookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: copyChildren,
+ });
+
+ let targetBookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: targetChildren,
+ });
+
+ ContentTree.view.selectItems([
+ copyBookmarks[0].guid,
+ copyBookmarks[3].guid,
+ copyBookmarks[6].guid,
+ copyBookmarks[9].guid,
+ ]);
+
+ await promiseClipboard(() => {
+ info("Cutting multiple selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ ContentTree.view.selectItems([targetBookmarks[4].guid]);
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ let tree = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+
+ const expectedBookmarkOrder = [
+ targetBookmarks[0].guid,
+ targetBookmarks[1].guid,
+ targetBookmarks[2].guid,
+ targetBookmarks[3].guid,
+ copyBookmarks[0].guid,
+ copyBookmarks[3].guid,
+ copyBookmarks[6].guid,
+ copyBookmarks[9].guid,
+ targetBookmarks[4].guid,
+ targetBookmarks[5].guid,
+ targetBookmarks[6].guid,
+ targetBookmarks[7].guid,
+ targetBookmarks[8].guid,
+ targetBookmarks[9].guid,
+ ];
+
+ Assert.equal(
+ tree.children.length,
+ expectedBookmarkOrder.length,
+ "Should be the expected amount of bookmarks in the unfiled folder."
+ );
+
+ for (let i = 0; i < expectedBookmarkOrder.length; ++i) {
+ Assert.equal(
+ tree.children[i].guid,
+ expectedBookmarkOrder[i],
+ `Should be the expected item at index ${i}`
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function paste_check_indexes_same_folder() {
+ info("Selecting BookmarksToolbar in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+
+ let copyChildren = [];
+ for (let i = 0; i < 10; i++) {
+ copyChildren.push({
+ url: `${TEST_URL}${i}`,
+ title: `Copy ${i}`,
+ });
+ }
+
+ let copyBookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: copyChildren,
+ });
+
+ ContentTree.view.selectItems([
+ copyBookmarks[0].guid,
+ copyBookmarks[3].guid,
+ copyBookmarks[6].guid,
+ copyBookmarks[9].guid,
+ ]);
+
+ await promiseClipboard(() => {
+ info("Cutting multiple selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ ContentTree.view.selectItems([copyBookmarks[4].guid]);
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ let tree = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.toolbarGuid
+ );
+
+ // Although we've inserted at index 4, we've taken out two items below it, so
+ // we effectively insert after the third item.
+ const expectedBookmarkOrder = [
+ copyBookmarks[1].guid,
+ copyBookmarks[2].guid,
+ copyBookmarks[0].guid,
+ copyBookmarks[3].guid,
+ copyBookmarks[6].guid,
+ copyBookmarks[9].guid,
+ copyBookmarks[4].guid,
+ copyBookmarks[5].guid,
+ copyBookmarks[7].guid,
+ copyBookmarks[8].guid,
+ ];
+
+ Assert.equal(
+ tree.children.length,
+ expectedBookmarkOrder.length,
+ "Should be the expected amount of bookmarks in the unfiled folder."
+ );
+
+ for (let i = 0; i < expectedBookmarkOrder.length; ++i) {
+ Assert.equal(
+ tree.children[i].guid,
+ expectedBookmarkOrder[i],
+ `Should be the expected item at index ${i}`
+ );
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function paste_from_different_instance() {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ xferable.init(null);
+
+ // Fake data on the clipboard to pretend this is from a different instance
+ // of Firefox.
+ let data = {
+ title: "test",
+ id: 32,
+ instanceId: "FAKEFAKEFAKE",
+ itemGuid: "ZBf_TYkrYGvW",
+ parent: 452,
+ dateAdded: 1464866275853000,
+ lastModified: 1507638113352000,
+ type: "text/x-moz-place",
+ uri: TEST_URL1,
+ };
+ data = JSON.stringify(data);
+
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE);
+ xferable.setTransferData(
+ PlacesUtils.TYPE_X_MOZ_PLACE,
+ PlacesUtils.toISupportsString(data)
+ );
+
+ Services.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ let tree = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+
+ Assert.equal(
+ tree.children.length,
+ 1,
+ "Should be one bookmark in the unfiled folder."
+ );
+ Assert.equal(tree.children[0].title, "test", "Should have the correct title");
+ Assert.equal(tree.children[0].uri, TEST_URL1, "Should have the correct URL");
+
+ await PlacesUtils.bookmarks.remove(tree.children[0].guid);
+});
+
+add_task(async function paste_separator_from_different_instance() {
+ let xferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(
+ Ci.nsITransferable
+ );
+ xferable.init(null);
+
+ // Fake data on the clipboard to pretend this is from a different instance
+ // of Firefox.
+ let data = {
+ title: "test",
+ id: 32,
+ instanceId: "FAKEFAKEFAKE",
+ itemGuid: "ZBf_TYkrYGvW",
+ parent: 452,
+ dateAdded: 1464866275853000,
+ lastModified: 1507638113352000,
+ type: PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
+ };
+ data = JSON.stringify(data);
+
+ xferable.addDataFlavor(PlacesUtils.TYPE_X_MOZ_PLACE);
+ xferable.setTransferData(
+ PlacesUtils.TYPE_X_MOZ_PLACE,
+ PlacesUtils.toISupportsString(data)
+ );
+
+ Services.clipboard.setData(xferable, null, Ci.nsIClipboard.kGlobalClipboard);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ let tree = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+
+ Assert.equal(
+ tree.children.length,
+ 1,
+ "Should be one bookmark in the unfiled folder."
+ );
+ Assert.equal(
+ tree.children[0].type,
+ PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
+ "Should have the correct type"
+ );
+
+ await PlacesUtils.bookmarks.remove(tree.children[0].guid);
+});
+
+add_task(async function paste_copy_check_indexes() {
+ info("Selecting BookmarksToolbar in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+
+ let copyChildren = [];
+ let targetChildren = [];
+ for (let i = 0; i < 10; i++) {
+ copyChildren.push({
+ url: `${TEST_URL}${i}`,
+ title: `Copy ${i}`,
+ });
+ targetChildren.push({
+ url: `${TEST_URL1}${i}`,
+ title: `Target ${i}`,
+ });
+ }
+
+ let copyBookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: copyChildren,
+ });
+
+ let targetBookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: targetChildren,
+ });
+
+ ContentTree.view.selectItems([
+ copyBookmarks[0].guid,
+ copyBookmarks[3].guid,
+ copyBookmarks[6].guid,
+ copyBookmarks[9].guid,
+ ]);
+
+ await promiseClipboard(() => {
+ info("Cutting multiple selection");
+ ContentTree.view.controller.copy();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ info("Selecting UnfiledBookmarks in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ ContentTree.view.selectItems([targetBookmarks[4].guid]);
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ let tree = await PlacesUtils.promiseBookmarksTree(
+ PlacesUtils.bookmarks.unfiledGuid
+ );
+
+ const expectedBookmarkOrder = [
+ targetBookmarks[0].guid,
+ targetBookmarks[1].guid,
+ targetBookmarks[2].guid,
+ targetBookmarks[3].guid,
+ 0,
+ 3,
+ 6,
+ 9,
+ targetBookmarks[4].guid,
+ targetBookmarks[5].guid,
+ targetBookmarks[6].guid,
+ targetBookmarks[7].guid,
+ targetBookmarks[8].guid,
+ targetBookmarks[9].guid,
+ ];
+
+ Assert.equal(
+ tree.children.length,
+ expectedBookmarkOrder.length,
+ "Should be the expected amount of bookmarks in the unfiled folder."
+ );
+
+ for (let i = 0; i < expectedBookmarkOrder.length; ++i) {
+ if (i > 3 && i <= 7) {
+ // Items 4 - 7 are copies of the original, so we need to compare data, rather
+ // than their guids.
+ Assert.equal(
+ tree.children[i].title,
+ copyChildren[expectedBookmarkOrder[i]].title,
+ `Should have the correct bookmark title at index ${i}`
+ );
+ Assert.equal(
+ tree.children[i].uri,
+ copyChildren[expectedBookmarkOrder[i]].url,
+ `Should have the correct bookmark URL at index ${i}`
+ );
+ } else {
+ Assert.equal(
+ tree.children[i].guid,
+ expectedBookmarkOrder[i],
+ `Should be the expected item at index ${i}`
+ );
+ }
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+});
diff --git a/browser/components/places/tests/browser/browser_paste_into_tags.js b/browser/components/places/tests/browser/browser_paste_into_tags.js
new file mode 100644
index 0000000000..52f312d68e
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_paste_into_tags.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+const TEST_URL = Services.io.newURI("http://example.com/");
+const MOZURISPEC = Services.io.newURI("http://mozilla.com/");
+
+add_task(async function () {
+ let organizer = await promiseLibrary();
+
+ ok(PlacesUtils, "PlacesUtils in scope");
+ ok(PlacesUIUtils, "PlacesUIUtils in scope");
+
+ let PlacesOrganizer = organizer.PlacesOrganizer;
+ ok(PlacesOrganizer, "Places organizer in scope");
+
+ let ContentTree = organizer.ContentTree;
+ ok(ContentTree, "ContentTree is in scope");
+
+ let visits = {
+ uri: MOZURISPEC,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ };
+ await PlacesTestUtils.addVisits(visits);
+
+ // create an initial tag to work with
+ let newBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ title: "bookmark/" + TEST_URL.spec,
+ url: TEST_URL,
+ });
+
+ ok(newBookmark, "A bookmark was added");
+ PlacesUtils.tagging.tagURI(TEST_URL, ["foo"]);
+ let tags = PlacesUtils.tagging.getTagsForURI(TEST_URL);
+ is(tags[0], "foo", "tag is foo");
+
+ // focus the new tag
+ focusTag(PlacesOrganizer);
+
+ let populate = () => copyHistNode(PlacesOrganizer, ContentTree);
+ await promiseClipboard(populate, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ focusTag(PlacesOrganizer);
+ await PlacesOrganizer._places.controller.paste();
+
+ // re-focus the history again
+ PlacesOrganizer.selectLeftPaneBuiltIn("History");
+ let histContainer = PlacesOrganizer._places.selectedNode;
+ PlacesUtils.asContainer(histContainer);
+ histContainer.containerOpen = true;
+ PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
+ ok(histNode, "histNode exists: " + histNode.title);
+
+ // check to see if the history node is tagged!
+ tags = PlacesUtils.tagging.getTagsForURI(MOZURISPEC);
+ Assert.equal(tags.length, 1, "history node is tagged: " + tags.length);
+
+ // check if a bookmark was created
+ let bookmarks = [];
+ await PlacesUtils.bookmarks.fetch({ url: MOZURISPEC }, bm => {
+ bookmarks.push(bm);
+ });
+ ok(!!bookmarks.length, "bookmark exists for the tagged history item");
+
+ // is the bookmark visible in the UI?
+ // get the Unsorted Bookmarks node
+ PlacesOrganizer.selectLeftPaneBuiltIn("UnfiledBookmarks");
+
+ // now we can see what is in the ContentTree tree
+ let unsortedNode = ContentTree.view.view.nodeForTreeIndex(1);
+ ok(unsortedNode, "unsortedNode is not null: " + unsortedNode.uri);
+ is(unsortedNode.uri, MOZURISPEC.spec, "node uri's are the same");
+
+ await promiseLibraryClosed(organizer);
+
+ // Remove new Places data we created.
+ PlacesUtils.tagging.untagURI(MOZURISPEC, ["foo"]);
+ PlacesUtils.tagging.untagURI(TEST_URL, ["foo"]);
+ tags = PlacesUtils.tagging.getTagsForURI(TEST_URL);
+ is(tags.length, 0, "tags are gone");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+});
+
+function focusTag(PlacesOrganizer) {
+ PlacesOrganizer.selectLeftPaneBuiltIn("Tags");
+ let tags = PlacesOrganizer._places.selectedNode;
+ tags.containerOpen = true;
+ let fooTag = tags.getChild(0);
+ let tagNode = fooTag;
+ PlacesOrganizer._places.selectNode(fooTag);
+ is(tagNode.title, "foo", "tagNode title is foo");
+ let ip = PlacesOrganizer._places.insertionPoint;
+ ok(ip.isTag, "IP is a tag");
+}
+
+function copyHistNode(PlacesOrganizer, ContentTree) {
+ // focus the history object
+ PlacesOrganizer.selectLeftPaneBuiltIn("History");
+ let histContainer = PlacesOrganizer._places.selectedNode;
+ PlacesUtils.asContainer(histContainer);
+ histContainer.containerOpen = true;
+ PlacesOrganizer._places.selectNode(histContainer.getChild(0));
+ let histNode = ContentTree.view.view.nodeForTreeIndex(0);
+ ContentTree.view.selectNode(histNode);
+ is(histNode.uri, MOZURISPEC.spec, "historyNode exists: " + histNode.uri);
+ // copy the history node
+ ContentTree.view.controller.copy();
+}
diff --git a/browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js b/browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js
new file mode 100644
index 0000000000..caa73d137f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = "http://example.com/";
+const TEST_URL1 = "https://example.com/otherbrowser/";
+
+var PlacesOrganizer;
+var ContentTree;
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ let organizer = await promiseLibrary();
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(organizer);
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+
+ PlacesOrganizer = organizer.PlacesOrganizer;
+ ContentTree = organizer.ContentTree;
+});
+
+add_task(async function paste() {
+ info("Selecting BookmarksToolbar in the left pane");
+ PlacesOrganizer.selectLeftPaneBuiltIn("BookmarksToolbar");
+
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ url: TEST_URL,
+ title: "0",
+ },
+ {
+ url: TEST_URL1,
+ title: "1",
+ },
+ ],
+ });
+
+ Assert.equal(
+ ContentTree.view.view.rowCount,
+ 2,
+ "Should have the right amount of items in the view"
+ );
+
+ ContentTree.view.selectItems([bookmarks[0].guid]);
+
+ await promiseClipboard(() => {
+ info("Cutting selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ assertItemsHighlighted(1);
+
+ ContentTree.view.selectItems([bookmarks[1].guid]);
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ assertItemsHighlighted(0);
+
+ // And now repeat the other way around to make sure.
+
+ await promiseClipboard(() => {
+ info("Cutting selection");
+ ContentTree.view.controller.cut();
+ }, PlacesUtils.TYPE_X_MOZ_PLACE);
+
+ assertItemsHighlighted(1);
+
+ ContentTree.view.selectItems([bookmarks[0].guid]);
+
+ info("Pasting clipboard");
+ await ContentTree.view.controller.paste();
+
+ assertItemsHighlighted(0);
+});
+
+function assertItemsHighlighted(expectedItems) {
+ let column = ContentTree.view.view._tree.columns[0];
+ // Check the properties of the cells to make sure nothing has a cut highlight.
+ let highlighedItems = 0;
+ for (let i = 0; i < ContentTree.view.view.rowCount; i++) {
+ if (
+ ContentTree.view.view.getCellProperties(i, column).includes("cutting")
+ ) {
+ highlighedItems++;
+ }
+ }
+
+ Assert.equal(
+ highlighedItems,
+ expectedItems,
+ "Should have the correct amount of items highlighed"
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_remove_bookmarks.js b/browser/components/places/tests/browser/browser_remove_bookmarks.js
new file mode 100644
index 0000000000..8889ea11c0
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_remove_bookmarks.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * Test removing bookmarks from the Bookmarks Toolbar and Library.
+ */
+
+const TEST_URL = "about:mozilla";
+
+add_setup(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ registerCleanupFunction(async () => {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_remove_bookmark_from_toolbar() {
+ let toolbarBookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ title: "Bookmark Title",
+ url: TEST_URL,
+ });
+
+ let toolbarNode = getToolbarNodeForItemGuid(toolbarBookmark.guid);
+
+ let contextMenu = document.getElementById("placesContext");
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ EventUtils.synthesizeMouseAtCenter(toolbarNode, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await popupShownPromise;
+
+ let contextMenuDeleteBookmark = document.getElementById(
+ "placesContext_deleteBookmark"
+ );
+
+ let removePromise = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => event.url == TEST_URL)
+ );
+
+ contextMenu.activateItem(contextMenuDeleteBookmark, {});
+
+ await removePromise;
+
+ Assert.deepEqual(
+ PlacesUtils.bookmarks.fetch({ url: TEST_URL }),
+ {},
+ "Should have removed the bookmark from the database"
+ );
+});
+
+add_task(async function test_remove_bookmark_from_library() {
+ const uris = [
+ "http://example.com/1",
+ "http://example.com/2",
+ "http://example.com/3",
+ ];
+
+ let children = uris.map((uri, index) => {
+ return {
+ title: `bm${index}`,
+ url: uri,
+ };
+ });
+
+ // Insert bookmarks.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children,
+ });
+
+ // Open the Library and select the "UnfiledBookmarks".
+ let library = await promiseLibrary("UnfiledBookmarks");
+
+ registerCleanupFunction(async function () {
+ await promiseLibraryClosed(library);
+ });
+
+ let PO = library.PlacesOrganizer;
+
+ Assert.equal(
+ PlacesUtils.getConcreteItemGuid(PO._places.selectedNode),
+ PlacesUtils.bookmarks.unfiledGuid,
+ "Should have selected unfiled bookmarks."
+ );
+
+ let contextMenu = library.document.getElementById("placesContext");
+ let contextMenuDeleteBookmark = library.document.getElementById(
+ "placesContext_deleteBookmark"
+ );
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ let firstColumn = library.ContentTree.view.columns[0];
+ let firstBookmarkRect = library.ContentTree.view.getCoordsForCellItem(
+ 0,
+ firstColumn,
+ "bm0"
+ );
+
+ EventUtils.synthesizeMouse(
+ library.ContentTree.view.body,
+ firstBookmarkRect.x,
+ firstBookmarkRect.y,
+ { type: "contextmenu", button: 2 },
+ library
+ );
+
+ await popupShownPromise;
+
+ Assert.equal(
+ library.ContentTree.view.result.root.childCount,
+ 3,
+ "Number of bookmarks before removal is right"
+ );
+
+ let removePromise = PlacesTestUtils.waitForNotification(
+ "bookmark-removed",
+ events => events.some(event => event.url == uris[0])
+ );
+ contextMenu.activateItem(contextMenuDeleteBookmark, {});
+
+ await removePromise;
+
+ Assert.equal(
+ library.ContentTree.view.result.root.childCount,
+ 2,
+ "Should have removed the bookmark from the display"
+ );
+});
diff --git a/browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js b/browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js
new file mode 100644
index 0000000000..10f82db286
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js
@@ -0,0 +1,141 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+let bookmarks;
+let folder;
+
+add_setup(async function () {
+ folder = await PlacesUtils.bookmarks.insert({
+ title: "Sidebar Test Folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: folder.guid,
+ children: [
+ {
+ title: "Mozilla",
+ url: "https://www.mozilla.org/",
+ },
+ {
+ title: "Example",
+ url: "https://sidebar.mozilla.org/",
+ },
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_open_multiple_bookmarks() {
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([folder.guid]);
+
+ is(
+ tree.selectedNode.title,
+ "Sidebar Test Folder",
+ "The sidebar test bookmarks folder is selected"
+ );
+
+ // open all bookmarks in this folder (which is two)
+ synthesizeClickOnSelectedTreeCell(tree, { button: 1 });
+
+ // expand the "Other bookmarks" folder
+ synthesizeClickOnSelectedTreeCell(tree, { button: 0 });
+ tree.selectItems([bookmarks[0].guid]);
+
+ is(tree.selectedNode.title, "Mozilla", "The first bookmark is selected");
+
+ synthesizeClickOnSelectedTreeCell(tree, { button: 0 });
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true),
+ "sidebar.link",
+ "bookmarks",
+ 3
+ );
+
+ let newWinOpened = BrowserTestUtils.waitForNewWindow();
+ // open a bookmark in new window via context menu
+ synthesizeClickOnSelectedTreeCell(tree, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ let openNewWindowOption = document.getElementById(
+ "placesContext_open:newwindow"
+ );
+ openNewWindowOption.click();
+
+ let newWin = await newWinOpened;
+
+ // total bookmarks opened
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "bookmarks",
+ 4
+ );
+
+ Services.telemetry.clearScalars();
+ BrowserTestUtils.closeWindow(newWin);
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+ });
+});
+
+add_task(async function test_bookmarks_search() {
+ let cumulativeSearchesHistogram = TelemetryTestUtils.getAndClearHistogram(
+ "PLACES_BOOKMARKS_SEARCHBAR_CUMULATIVE_SEARCHES"
+ );
+ cumulativeSearchesHistogram.clear();
+
+ await withSidebarTree("bookmarks", async tree => {
+ // Search the tree.
+ let searchBox = tree.ownerDocument.getElementById("search-box");
+ searchBox.value = "example";
+ searchBox.doCommand();
+
+ searchBox.value = "";
+ searchBox.doCommand();
+ info("Search was reset");
+
+ // Perform a second search.
+ searchBox.value = "mozilla";
+ searchBox.doCommand();
+ info("Second search was performed");
+
+ // Select the first link and click on it.
+ tree.selectNode(tree.view.nodeForTreeIndex(0));
+
+ synthesizeClickOnSelectedTreeCell(tree, { button: 0 });
+ info("First link was selected and then clicked on");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "bookmarks",
+ 1
+ );
+ });
+
+ TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 2, 1);
+ info("Cumulative search probe is recorded");
+
+ cumulativeSearchesHistogram.clear();
+ Services.telemetry.clearScalars();
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_sidebar_history_telemetry.js b/browser/components/places/tests/browser/browser_sidebar_history_telemetry.js
new file mode 100644
index 0000000000..cb27bf66ef
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sidebar_history_telemetry.js
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+const firstNodeIndex = 0;
+
+// The prompt returns 1 for cancelled and 0 for accepted.
+let gResponse = 1;
+(function replacePromptService() {
+ let originalPromptService = Services.prompt;
+ Services.prompt = {
+ QueryInterface: ChromeUtils.generateQI([Ci.nsIPromptService]),
+ confirmEx: () => gResponse,
+ };
+ registerCleanupFunction(() => {
+ Services.prompt = originalPromptService;
+ });
+})();
+
+add_setup(async function () {
+ await PlacesUtils.history.clear();
+
+ // Visited pages listed by descending visit date.
+ let pages = [
+ "https://sidebar.mozilla.org/a",
+ "https://sidebar.mozilla.org/b",
+ "https://sidebar.mozilla.org/c",
+ "https://www.mozilla.org/d",
+ ];
+
+ // Add some visited page.
+ let time = Date.now();
+ let places = [];
+ for (let i = 0; i < pages.length; i++) {
+ places.push({
+ uri: NetUtil.newURI(pages[i]),
+ visitDate: (time - i) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ });
+ }
+ await PlacesTestUtils.addVisits(places);
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_click_multiple_history_entries() {
+ await withSidebarTree("history", async tree => {
+ tree.ownerDocument.getElementById("byday").doCommand();
+
+ // Select the first link and click on it.
+ tree.selectNode(tree.view.nodeForTreeIndex(firstNodeIndex));
+ is(
+ tree.selectedNode.title,
+ "Today",
+ "The Today history sidebar button is selected"
+ );
+
+ // if a user tries to open all history items and they cancel opening
+ // those items in new tabs (due to max tab limit warning),
+ // no telemetry should be recorded
+ gResponse = 1;
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.tabs.maxOpenBeforeWarn", 4]],
+ });
+
+ // open multiple history items with a single click
+ synthesizeClickOnSelectedTreeCell(tree, { button: 1 });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link"
+ );
+
+ // if they proceed with opening history multiple history items despite the warning,
+ // telemetry should be recorded
+ gResponse = 0;
+ synthesizeClickOnSelectedTreeCell(tree, { button: 1 });
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "history",
+ 4
+ );
+
+ let parentNode = tree.selectedNode;
+ if (!parentNode.containerOpen) {
+ // Only need to open/expand container node on first run
+ synthesizeClickOnSelectedTreeCell(tree);
+ }
+ if (parentNode.title == "Today" && parentNode.hasChildren) {
+ info(`Selecting node with title ${parentNode?.getChild(0)?.title}`);
+ tree.selectNode(parentNode.getChild(0));
+ }
+
+ synthesizeClickOnSelectedTreeCell(tree, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link"
+ );
+
+ let openNewTabOption = document.getElementById("placesContext_open:newtab");
+ openNewTabOption.click();
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "history",
+ 1
+ );
+
+ if (parentNode.title == "Today" && parentNode.hasChildren) {
+ tree.selectNode(parentNode.getChild(0));
+ }
+
+ let newWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ synthesizeClickOnSelectedTreeCell(tree, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link"
+ );
+
+ let openNewWindowOption = document.getElementById(
+ "placesContext_open:newwindow"
+ );
+ openNewWindowOption.click();
+
+ let newWin = await newWinOpened;
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "history",
+ 1
+ );
+
+ await BrowserTestUtils.closeWindow(newWin);
+
+ if (parentNode.title == "Today" && parentNode.hasChildren) {
+ tree.selectNode(parentNode.getChild(0));
+ }
+
+ let newPrivateWinOpened = BrowserTestUtils.waitForNewWindow();
+
+ synthesizeClickOnSelectedTreeCell(tree, {
+ button: 2,
+ type: "contextmenu",
+ });
+
+ TelemetryTestUtils.assertScalarUnset(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link"
+ );
+
+ let openNewPrivateWindowOption = document.getElementById(
+ "placesContext_open:newprivatewindow"
+ );
+ openNewPrivateWindowOption.click();
+
+ let newPrivateWin = await newPrivateWinOpened;
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "history",
+ 1
+ );
+
+ await BrowserTestUtils.closeWindow(newPrivateWin);
+ });
+
+ await SpecialPowers.popPrefEnv();
+
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
+
+add_task(async function test_search_and_filter() {
+ let cumulativeSearchesHistogram = Services.telemetry.getHistogramById(
+ "PLACES_SEARCHBAR_CUMULATIVE_SEARCHES"
+ );
+ cumulativeSearchesHistogram.clear();
+ let cumulativeFilterCountHistogram = Services.telemetry.getHistogramById(
+ "PLACES_SEARCHBAR_CUMULATIVE_FILTER_COUNT"
+ );
+ cumulativeFilterCountHistogram.clear();
+
+ await withSidebarTree("history", async tree => {
+ // Apply a search filter.
+ tree.ownerDocument.getElementById("bylastvisited").doCommand();
+ info("Search filter was changed to bylastvisited");
+
+ // Search the tree.
+ let searchBox = tree.ownerDocument.getElementById("search-box");
+ searchBox.value = "sidebar.mozilla";
+ searchBox.doCommand();
+ info("Tree was searched with sting sidebar.mozilla");
+
+ searchBox.value = "";
+ searchBox.doCommand();
+ info("Search was reset");
+
+ // Perform a second search.
+ searchBox.value = "sidebar.mozilla";
+ searchBox.doCommand();
+ info("Second search was performed");
+
+ // Select the first link and click on it.
+ tree.selectNode(tree.view.nodeForTreeIndex(firstNodeIndex));
+ synthesizeClickOnSelectedTreeCell(tree, { button: 1 });
+ info("First link was selected and then clicked on");
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "history",
+ 1
+ );
+ });
+
+ TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 2, 1);
+ info("Cumulative search telemetry looks right");
+
+ TelemetryTestUtils.assertHistogram(cumulativeFilterCountHistogram, 1, 1);
+ info("Cumulative search filter telemetry looks right");
+
+ cumulativeSearchesHistogram.clear();
+ cumulativeFilterCountHistogram.clear();
+
+ await withSidebarTree("history", async tree => {
+ // Apply a search filter.
+ tree.ownerDocument.getElementById("byday").doCommand();
+ info("First search filter applied");
+
+ // Apply another search filter.
+ tree.ownerDocument.getElementById("bylastvisited").doCommand();
+ info("Second search filter applied");
+
+ // Apply a search filter.
+ tree.ownerDocument.getElementById("byday").doCommand();
+ info("Third search filter applied");
+
+ // Search the tree.
+ let searchBox = tree.ownerDocument.getElementById("search-box");
+ searchBox.value = "sidebar.mozilla";
+ searchBox.doCommand();
+
+ // Select the first link and click on it.
+ tree.selectNode(tree.view.nodeForTreeIndex(firstNodeIndex));
+ synthesizeClickOnSelectedTreeCell(tree, { button: 1 });
+
+ TelemetryTestUtils.assertKeyedScalar(
+ TelemetryTestUtils.getProcessScalars("parent", true, true),
+ "sidebar.link",
+ "history",
+ 1
+ );
+ });
+
+ TelemetryTestUtils.assertHistogram(cumulativeSearchesHistogram, 1, 1);
+ TelemetryTestUtils.assertHistogram(cumulativeFilterCountHistogram, 3, 1);
+
+ cumulativeSearchesHistogram.clear();
+ cumulativeFilterCountHistogram.clear();
+ while (gBrowser.tabs.length > 1) {
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_sidebar_on_customization.js b/browser/components/places/tests/browser/browser_sidebar_on_customization.js
new file mode 100644
index 0000000000..6e97f81dd3
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sidebar_on_customization.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gInsertedBookmarks;
+add_setup(async function () {
+ gInsertedBookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "bm1",
+ url: "about:buildconfig",
+ },
+ {
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "folder",
+ children: [
+ {
+ title: "bm2",
+ url: "about:mozilla",
+ },
+ ],
+ },
+ ],
+ });
+ registerCleanupFunction(PlacesUtils.bookmarks.eraseEverything);
+});
+
+add_task(async function test_open_sidebar_and_customize() {
+ await withSidebarTree("bookmarks", async tree => {
+ async function checkTreeIsFunctional() {
+ Assert.ok(SidebarUI.isOpen, "Sidebar is open");
+ Assert.ok(
+ BrowserTestUtils.isVisible(SidebarUI.browser),
+ "sidebar browser is visible"
+ );
+ Assert.ok(tree.view.result, "View result is defined");
+ await TestUtils.waitForCondition(
+ () => tree.view.result.root.containerOpen,
+ "View root node should be reopened"
+ );
+ toggleFolder(tree, gInsertedBookmarks[1].guid);
+ }
+
+ await checkTreeIsFunctional();
+
+ info("Starting customization");
+ await promiseCustomizeStart();
+
+ Assert.ok(
+ !BrowserTestUtils.isVisible(SidebarUI.browser),
+ "sidebar browser is hidden"
+ );
+ Assert.ok(tree.view.result, "View result is defined");
+ Assert.ok(!tree.view.result.root.containerOpen, "View root node is closed");
+
+ info("Ending customization");
+ await promiseCustomizeEnd();
+
+ await checkTreeIsFunctional();
+ });
+});
+
+function promiseCustomizeStart(win = window) {
+ return new Promise(resolve => {
+ win.gNavToolbox.addEventListener("customizationready", resolve, {
+ once: true,
+ });
+ win.gCustomizeMode.enter();
+ });
+}
+
+function promiseCustomizeEnd(win = window) {
+ return new Promise(resolve => {
+ win.gNavToolbox.addEventListener("aftercustomization", resolve, {
+ once: true,
+ });
+ win.gCustomizeMode.exit();
+ });
+}
+
+function toggleFolder(tree, guid) {
+ tree.selectItems([guid]);
+ Assert.equal(tree.selectedNode.title, "folder");
+ Assert.ok(
+ !PlacesUtils.asContainer(tree.selectedNode).containerOpen,
+ "Folder is closed"
+ );
+ synthesizeClickOnSelectedTreeCell(tree);
+ Assert.ok(
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen,
+ "Folder is open"
+ );
+ synthesizeClickOnSelectedTreeCell(tree);
+ Assert.ok(
+ !PlacesUtils.asContainer(tree.selectedNode).containerOpen,
+ "Folder is closed"
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js b/browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js
new file mode 100644
index 0000000000..92f98b898c
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const PREF_LOAD_BOOKMARKS_IN_TABS = "browser.tabs.loadBookmarksInTabs";
+
+var gBms;
+
+add_setup(async function () {
+ gBms = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "bm1",
+ url: "about:buildconfig",
+ },
+ {
+ title: "bm2",
+ url: "about:mozilla",
+ },
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_open_bookmark_from_sidebar() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([gBms[0].guid]);
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ gBms[0].url
+ );
+
+ tree.controller.doCommand("placesCmd_open");
+
+ await loadedPromise;
+
+ // An assert to make the test happy.
+ Assert.ok(true, "The bookmark was loaded successfully.");
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_open_bookmark_from_sidebar_keypress() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([gBms[1].guid]);
+
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ gBms[1].url
+ );
+
+ tree.focus();
+ EventUtils.sendKey("return");
+
+ await loadedPromise;
+
+ // An assert to make the test happy.
+ Assert.ok(true, "The bookmark was loaded successfully.");
+ });
+
+ await BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_open_bookmark_in_tab_from_sidebar() {
+ await SpecialPowers.pushPrefEnv({
+ set: [[PREF_LOAD_BOOKMARKS_IN_TABS, true]],
+ });
+
+ await BrowserTestUtils.withNewTab({ gBrowser }, async initialTab => {
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([gBms[0].guid]);
+ let loadedPromise = BrowserTestUtils.browserLoaded(
+ gBrowser.selectedBrowser,
+ false,
+ gBms[0].url
+ );
+ tree.focus();
+ EventUtils.sendKey("return");
+ await loadedPromise;
+ Assert.ok(true, "The bookmark reused the empty tab.");
+
+ tree.selectItems([gBms[1].guid]);
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, gBms[1].url);
+ tree.focus();
+ EventUtils.sendKey("return");
+ let newTab = await newTabPromise;
+ Assert.ok(true, "The bookmark was opened in a new tab.");
+ BrowserTestUtils.removeTab(newTab);
+ });
+ });
+
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_open_bookmark_folder_from_sidebar() {
+ await withSidebarTree("bookmarks", async tree => {
+ tree.selectItems([PlacesUtils.bookmarks.virtualUnfiledGuid]);
+
+ Assert.equal(
+ tree.view.selection.getRangeCount(),
+ 1,
+ "Should only have one range selected"
+ );
+
+ let loadedPromises = [];
+
+ for (let bm of gBms) {
+ loadedPromises.push(
+ BrowserTestUtils.waitForNewTab(gBrowser, bm.url, false, true)
+ );
+ }
+
+ synthesizeClickOnSelectedTreeCell(tree, { button: 1 });
+
+ let tabs = await Promise.all(loadedPromises);
+
+ for (let tab of tabs) {
+ await BrowserTestUtils.removeTab(tab);
+ }
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_sidebarpanels_click.js b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
new file mode 100644
index 0000000000..4b231c92b0
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sidebarpanels_click.js
@@ -0,0 +1,172 @@
+/* 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 makes sure that the items in the bookmarks and history sidebar
+// panels are clickable in both LTR and RTL modes.
+
+var sidebar;
+
+function pushPref(name, val) {
+ return SpecialPowers.pushPrefEnv({ set: [[name, val]] });
+}
+
+function popPref() {
+ return SpecialPowers.popPrefEnv();
+}
+
+add_task(async function test_sidebarpanels_click() {
+ ignoreAllUncaughtExceptions();
+
+ const BOOKMARKS_SIDEBAR_ID = "viewBookmarksSidebar";
+ const BOOKMARKS_SIDEBAR_TREE_ID = "bookmarks-view";
+ const HISTORY_SIDEBAR_ID = "viewHistorySidebar";
+ const HISTORY_SIDEBAR_TREE_ID = "historyTree";
+ const TEST_URL =
+ "http://mochi.test:8888/browser/browser/components/places/tests/browser/sidebarpanels_click_test_page.html";
+
+ // If a sidebar is already open, close it.
+ if (!document.getElementById("sidebar-box").hidden) {
+ ok(
+ false,
+ "Unexpected sidebar found - a previous test failed to cleanup correctly"
+ );
+ SidebarUI.hide();
+ }
+
+ // Ensure history is clean before starting the test.
+ await PlacesUtils.history.clear();
+
+ sidebar = document.getElementById("sidebar");
+ let tests = [];
+
+ tests.push({
+ _bookmark: null,
+ async init() {
+ // Add a bookmark to the Unfiled Bookmarks folder.
+ this._bookmark = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ title: "test",
+ url: TEST_URL,
+ });
+ },
+ prepare() {},
+ async selectNode(tree) {
+ tree.selectItems([this._bookmark.guid]);
+ },
+ cleanup(aCallback) {
+ return PlacesUtils.bookmarks.remove(this._bookmark);
+ },
+ sidebarName: BOOKMARKS_SIDEBAR_ID,
+ treeName: BOOKMARKS_SIDEBAR_TREE_ID,
+ desc: "Bookmarks sidebar test",
+ });
+
+ tests.push({
+ async init() {
+ // Add a history entry.
+ let uri = Services.io.newURI(TEST_URL);
+ await PlacesTestUtils.addVisits({
+ uri,
+ visitDate: Date.now() * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ });
+ },
+ prepare() {
+ sidebar.contentDocument.getElementById("byvisited").doCommand();
+ },
+ selectNode(tree) {
+ tree.selectNode(tree.view.nodeForTreeIndex(0));
+ is(
+ tree.selectedNode.uri,
+ TEST_URL,
+ "The correct visit has been selected"
+ );
+ is(tree.selectedNode.itemId, -1, "The selected node is not bookmarked");
+ },
+ cleanup(aCallback) {
+ return PlacesUtils.history.clear();
+ },
+ sidebarName: HISTORY_SIDEBAR_ID,
+ treeName: HISTORY_SIDEBAR_TREE_ID,
+ desc: "History sidebar test",
+ });
+
+ for (let test of tests) {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+ info("Running " + test.desc + " in LTR mode");
+ await testPlacesPanel(test);
+
+ await pushPref("intl.l10n.pseudo", "bidi");
+ info("Running " + test.desc + " in RTL mode");
+ await testPlacesPanel(test);
+ await popPref();
+
+ // Remove tabs created by sub-tests.
+ while (gBrowser.tabs.length > 1) {
+ gBrowser.removeTab(gBrowser.tabs[gBrowser.tabs.length - 1]);
+ }
+ }
+});
+
+async function testPlacesPanel(testInfo) {
+ await testInfo.init();
+
+ let promise = new Promise(resolve => {
+ sidebar.addEventListener(
+ "load",
+ function () {
+ executeSoon(async function () {
+ testInfo.prepare();
+
+ let tree = sidebar.contentDocument.getElementById(testInfo.treeName);
+
+ // Select the inserted places item.
+ await testInfo.selectNode(tree);
+
+ let promiseAlert = promiseAlertDialogObserved();
+
+ synthesizeClickOnSelectedTreeCell(tree);
+ // Now, wait for the observer to catch the alert dialog.
+ // If something goes wrong, the test will time out at this stage.
+ // Note that for the history sidebar, the URL itself is not opened,
+ // and Places will show the load-js-data-url-error prompt as an alert
+ // box, which means that the click actually worked, so it's good enough
+ // for the purpose of this test.
+
+ await promiseAlert;
+
+ executeSoon(async function () {
+ SidebarUI.hide();
+ await testInfo.cleanup();
+ resolve();
+ });
+ });
+ },
+ { capture: true, once: true }
+ );
+ });
+
+ SidebarUI.show(testInfo.sidebarName);
+
+ return promise;
+}
+
+function promiseAlertDialogObserved() {
+ return new Promise(resolve => {
+ async function observer(subject) {
+ info("alert dialog observed as expected");
+ Services.obs.removeObserver(observer, "common-dialog-loaded");
+ Services.obs.removeObserver(observer, "tabmodal-dialog-loaded");
+
+ if (subject.Dialog) {
+ subject.Dialog.ui.button0.click();
+ } else {
+ subject.querySelector(".tabmodalprompt-button0").click();
+ }
+ resolve();
+ }
+ Services.obs.addObserver(observer, "common-dialog-loaded");
+ Services.obs.addObserver(observer, "tabmodal-dialog-loaded");
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_sort_in_library.js b/browser/components/places/tests/browser/browser_sort_in_library.js
new file mode 100644
index 0000000000..2eea3a3f31
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_sort_in_library.js
@@ -0,0 +1,248 @@
+/* -*- 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 the following bugs:
+ *
+ * Bug 443745 - View>Sort>of "alpha" sort items is default to Z>A instead of A>Z
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=443745
+ *
+ * Bug 444179 - Library>Views>Sort>Sort by Tags does nothing
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=444179
+ *
+ * Basically, fully tests sorting the placeContent tree in the Places Library
+ * window. Sorting is verified by comparing the nsINavHistoryResult returned by
+ * placeContent.result to the expected sort values.
+ */
+
+// Two properties of nsINavHistoryResult control the sort of the tree:
+// sortingMode. sortingMode's value is one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants.
+//
+// This lookup table maps the possible values of anonid's of the treecols to
+// objects that represent the treecols' correct state after the user sorts the
+// previously unsorted tree by selecting a column from the Views > Sort menu.
+// sortingMode is constructed from the key and dir properties (i.e.,
+// SORT_BY_<key>_<dir>).
+const SORT_LOOKUP_TABLE = {
+ title: { key: "TITLE", dir: "ASCENDING" },
+ tags: { key: "TAGS", dir: "ASCENDING" },
+ url: { key: "URI", dir: "ASCENDING" },
+ date: { key: "DATE", dir: "DESCENDING" },
+ visitCount: { key: "VISITCOUNT", dir: "DESCENDING" },
+ dateAdded: { key: "DATEADDED", dir: "DESCENDING" },
+ lastModified: { key: "LASTMODIFIED", dir: "DESCENDING" },
+};
+
+// This is the column that's sorted if one is not specified and the tree is
+// currently unsorted. Set it to a key substring in the name of one of the
+// nsINavHistoryQueryOptions.SORT_BY_* constants, e.g., "TITLE", "URI".
+// Method ViewMenu.setSortColumn in browser/components/places/content/places.js
+// determines this value.
+const DEFAULT_SORT_KEY = "TITLE";
+
+// Part of the test is checking that sorts stick, so each time we sort we need
+// to remember it.
+var prevSortDir = null;
+var prevSortKey = null;
+
+/**
+ * Ensures that the sort of aTree is aSortingMode
+ *
+ * @param {object} aTree
+ * the tree to check
+ * @param {Ci.nsINavHistoryQueryOptions} aSortingMode
+ * one of the Ci.nsINavHistoryQueryOptions.SORT_BY_* constants
+ */
+function checkSort(aTree, aSortingMode) {
+ // The placeContent tree's sort is determined by the nsINavHistoryResult it
+ // stores. Get it and check that the sort is what the caller expects.
+ let res = aTree.result;
+ isnot(res, null, "sanity check: placeContent.result should not return null");
+
+ // Check sortingMode.
+ is(
+ res.sortingMode,
+ aSortingMode,
+ "column should now have sortingMode " + aSortingMode
+ );
+}
+
+/**
+ * Sets the sort of aTree.
+ *
+ * @param {object} aOrganizerWin
+ * the Places window
+ * @param {object} aTree
+ * the tree to sort
+ * @param {boolean} aUnsortFirst
+ * true if the sort should be set to SORT_BY_NONE before sorting by aCol
+ * and aDir
+ * @param {boolean} aShouldFail
+ * true if setSortColumn should fail on aCol or aDir
+ * @param {object} aCol
+ * the column of aTree by which to sort
+ * @param {string} aDir
+ * either "ascending" or "descending"
+ */
+function setSort(aOrganizerWin, aTree, aUnsortFirst, aShouldFail, aCol, aDir) {
+ if (aUnsortFirst) {
+ aOrganizerWin.ViewMenu.setSortColumn();
+ checkSort(aTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+
+ // Remember the sort key and direction.
+ prevSortKey = null;
+ prevSortDir = null;
+ }
+
+ let failed = false;
+ try {
+ aOrganizerWin.ViewMenu.setSortColumn(aCol, aDir);
+
+ // Remember the sort key and direction.
+ if (!aCol && !aDir) {
+ prevSortKey = null;
+ prevSortDir = null;
+ } else {
+ if (aCol) {
+ prevSortKey = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].key;
+ } else if (prevSortKey === null) {
+ prevSortKey = DEFAULT_SORT_KEY;
+ }
+
+ if (aDir) {
+ prevSortDir = aDir.toUpperCase();
+ } else if (prevSortDir === null) {
+ prevSortDir = SORT_LOOKUP_TABLE[aCol.getAttribute("anonid")].dir;
+ }
+ }
+ } catch (exc) {
+ failed = true;
+ }
+
+ is(
+ failed,
+ !!aShouldFail,
+ "setSortColumn on column " +
+ (aCol ? aCol.getAttribute("anonid") : "(no column)") +
+ " with direction " +
+ (aDir || "(no direction)") +
+ " and table previously " +
+ (aUnsortFirst ? "unsorted" : "sorted") +
+ " should " +
+ (aShouldFail ? "" : "not ") +
+ "fail"
+ );
+}
+
+/**
+ * Tries sorting by an invalid column and sort direction.
+ *
+ * @param {object} aOrganizerWin
+ * the Places window
+ * @param {object} aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ */
+function testInvalid(aOrganizerWin, aPlaceContentTree) {
+ // Invalid column should fail by throwing an exception.
+ let bogusCol = document.createXULElement("treecol");
+ bogusCol.setAttribute("anonid", "bogusColumn");
+ setSort(aOrganizerWin, aPlaceContentTree, true, true, bogusCol, "ascending");
+
+ // Invalid direction reverts to SORT_BY_NONE.
+ setSort(aOrganizerWin, aPlaceContentTree, false, false, null, "bogus dir");
+ checkSort(aPlaceContentTree, Ci.nsINavHistoryQueryOptions.SORT_BY_NONE, "");
+}
+
+/**
+ * Tests sorting aPlaceContentTree by column only and then by both column
+ * and direction.
+ *
+ * @param {object} aOrganizerWin
+ * the Places window
+ * @param {object} aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param {boolean} aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByColAndDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ let cols = aPlaceContentTree.getElementsByTagName("treecol");
+ ok(!!cols.length, "sanity check: placeContent should contain columns");
+
+ for (let i = 0; i < cols.length; i++) {
+ let col = cols.item(i);
+ ok(
+ col.hasAttribute("anonid"),
+ "sanity check: column " + col.id + " should have anonid"
+ );
+
+ let colId = col.getAttribute("anonid");
+ ok(
+ colId in SORT_LOOKUP_TABLE,
+ "sanity check: unexpected placeContent column anonid"
+ );
+
+ let sortStr =
+ "SORT_BY_" +
+ SORT_LOOKUP_TABLE[colId].key +
+ "_" +
+ (aUnsortFirst ? SORT_LOOKUP_TABLE[colId].dir : prevSortDir);
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortStr];
+
+ // Test sorting by only a column.
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col);
+ checkSort(aPlaceContentTree, expectedSortMode);
+
+ // Test sorting by both a column and a direction.
+ ["ascending", "descending"].forEach(function (dir) {
+ sortStr =
+ "SORT_BY_" + SORT_LOOKUP_TABLE[colId].key + "_" + dir.toUpperCase();
+ expectedSortMode = Ci.nsINavHistoryQueryOptions[sortStr];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, col, dir);
+ checkSort(aPlaceContentTree, expectedSortMode);
+ });
+ }
+}
+
+/**
+ * Tests sorting aPlaceContentTree by direction only.
+ *
+ * @param {object} aOrganizerWin
+ * the Places window
+ * @param {object} aPlaceContentTree
+ * the placeContent tree in aOrganizerWin
+ * @param {boolean} aUnsortFirst
+ * true if, before each sort we try, we should sort to SORT_BY_NONE
+ */
+function testSortByDir(aOrganizerWin, aPlaceContentTree, aUnsortFirst) {
+ ["ascending", "descending"].forEach(function (dir) {
+ let key = aUnsortFirst ? DEFAULT_SORT_KEY : prevSortKey;
+ let sortStr = "SORT_BY_" + key + "_" + dir.toUpperCase();
+ let expectedSortMode = Ci.nsINavHistoryQueryOptions[sortStr];
+ setSort(aOrganizerWin, aPlaceContentTree, aUnsortFirst, false, null, dir);
+ checkSort(aPlaceContentTree, expectedSortMode, "");
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ openLibrary(function (win) {
+ let tree = win.document.getElementById("placeContent");
+ isnot(tree, null, "sanity check: placeContent tree should exist");
+ // Run the tests.
+ testSortByColAndDir(win, tree, true);
+ testSortByColAndDir(win, tree, false);
+ testSortByDir(win, tree, true);
+ testSortByDir(win, tree, false);
+ testInvalid(win, tree);
+ // Reset the sort to SORT_BY_NONE.
+ setSort(win, tree, false, false);
+ // Close the window and finish.
+ win.close();
+ finish();
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_stayopenmenu.js b/browser/components/places/tests/browser/browser_stayopenmenu.js
new file mode 100644
index 0000000000..ec26700ef7
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_stayopenmenu.js
@@ -0,0 +1,267 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Menus should stay open (if pref is set) after ctrl-click, middle-click,
+// and contextmenu's "Open in a new tab" click.
+
+async function locateBookmarkAndTestCtrlClick(menupopup) {
+ let testMenuitem = [...menupopup.children].find(
+ node => node.label == "Test1"
+ );
+ ok(testMenuitem, "Found test bookmark.");
+ ok(BrowserTestUtils.isVisible(testMenuitem), "Should be visible");
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ EventUtils.synthesizeMouseAtCenter(testMenuitem, { accelKey: true });
+ let newTab = await promiseTabOpened;
+ ok(true, "Bookmark ctrl-click opened new tab.");
+ BrowserTestUtils.removeTab(newTab);
+ return testMenuitem;
+}
+
+async function testContextmenu(menuitem) {
+ let doc = menuitem.ownerDocument;
+ let cm = doc.getElementById("placesContext");
+ let promiseEvent = BrowserTestUtils.waitForEvent(cm, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(menuitem, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await promiseEvent;
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ let hidden = BrowserTestUtils.waitForEvent(cm, "popuphidden");
+ cm.activateItem(doc.getElementById("placesContext_open:newtab"));
+ await hidden;
+ let newTab = await promiseTabOpened;
+ return newTab;
+}
+
+add_setup(async function () {
+ // Ensure BMB is available in UI.
+ let origBMBlocation = CustomizableUI.getPlacementOfWidget(
+ "bookmarks-menu-button"
+ );
+ if (!origBMBlocation) {
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ }
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.bookmarks.openInTabClosesMenu", false]],
+ });
+ // Ensure menubar visible.
+ let menubar = document.getElementById("toolbar-menubar");
+ let menubarVisible = isToolbarVisible(menubar);
+ if (!menubarVisible) {
+ setToolbarVisibility(menubar, true);
+ info("Menubar made visible");
+ }
+ // Ensure Bookmarks Toolbar Visible.
+ let toolbar = document.getElementById("PersonalToolbar");
+ let toolbarHidden = toolbar.collapsed;
+ if (toolbarHidden) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ info("Bookmarks toolbar made visible");
+ }
+ // Create our test bookmarks.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ url: "http://example.com/",
+ title: "Test1",
+ });
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "TEST_TITLE",
+ index: 0,
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ url: "http://example.com/",
+ title: "Test1",
+ });
+
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.eraseEverything();
+ // if BMB was not originally in UI, remove it.
+ if (!origBMBlocation) {
+ CustomizableUI.removeWidgetFromArea("bookmarks-menu-button");
+ }
+ // Restore menubar to original visibility.
+ setToolbarVisibility(menubar, menubarVisible);
+ // Restore original bookmarks toolbar visibility.
+ if (toolbarHidden) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ });
+});
+
+add_task(async function testStayopenBookmarksClicks() {
+ // Test Bookmarks Menu Button stayopen clicks - Ctrl-click.
+ let BMB = document.getElementById("bookmarks-menu-button");
+ let BMBpopup = document.getElementById("BMB_bookmarksPopup");
+ let promiseEvent = BrowserTestUtils.waitForEvent(BMBpopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(BMB, {});
+ await promiseEvent;
+ info("Popupshown on Bookmarks-Menu-Button");
+ var menuitem = await locateBookmarkAndTestCtrlClick(BMBpopup);
+ ok(BMB.open, "Bookmarks Menu Button's Popup should still be open.");
+
+ // Test Bookmarks Menu Button stayopen clicks: middle-click.
+ let promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ EventUtils.synthesizeMouseAtCenter(menuitem, { button: 1 });
+ let newTab = await promiseTabOpened;
+ ok(true, "Bookmark middle-click opened new tab.");
+ BrowserTestUtils.removeTab(newTab);
+ ok(BMB.open, "Bookmarks Menu Button's Popup should still be open.");
+
+ // Test Bookmarks Menu Button stayopen clicks - 'Open in new tab' on context menu.
+ newTab = await testContextmenu(menuitem);
+ ok(true, "Bookmark contextmenu opened new tab.");
+ ok(BMB.open, "Bookmarks Menu Button's Popup should still be open.");
+ promiseEvent = BrowserTestUtils.waitForEvent(BMBpopup, "popuphidden");
+ BMB.open = false;
+ await promiseEvent;
+ info("Closing menu");
+ BrowserTestUtils.removeTab(newTab);
+
+ // Test App Menu's Bookmarks Library stayopen clicks.
+ let appMenu = document.getElementById("PanelUI-menu-button");
+ let appMenuPopup = document.getElementById("appMenu-popup");
+ let PopupShownPromise = BrowserTestUtils.waitForEvent(
+ appMenuPopup,
+ "popupshown"
+ );
+ appMenu.click();
+ await PopupShownPromise;
+
+ let BMview;
+ document.getElementById("appMenu-bookmarks-button").click();
+ BMview = document.getElementById("PanelUI-bookmarks");
+ let promise = BrowserTestUtils.waitForEvent(BMview, "ViewShown");
+ await promise;
+ info("Bookmarks panel shown.");
+
+ // Test App Menu's Bookmarks Library stayopen clicks: Ctrl-click.
+ let menu = document.getElementById("panelMenu_bookmarksMenu");
+ var testMenuitem = await locateBookmarkAndTestCtrlClick(menu);
+ ok(appMenu.open, "Menu should remain open.");
+
+ // Test App Menu's Bookmarks Library stayopen clicks: middle-click.
+ promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ EventUtils.synthesizeMouseAtCenter(testMenuitem, { button: 1 });
+ newTab = await promiseTabOpened;
+ ok(true, "Bookmark middle-click opened new tab.");
+ BrowserTestUtils.removeTab(newTab);
+ ok(
+ PanelView.forNode(BMview).active,
+ "Should still show the bookmarks subview"
+ );
+ ok(appMenu.open, "Menu should remain open.");
+
+ // Close the App Menu
+ appMenuPopup.hidePopup();
+ ok(!appMenu.open, "The menu should now be closed.");
+
+ // Disable the rest of the tests on Mac due to Mac's handling of menus being
+ // slightly different to the other platforms.
+ if (AppConstants.platform === "macosx") {
+ return;
+ }
+
+ // Test Bookmarks Menu (menubar) stayopen clicks: Ctrl-click.
+ let BM = document.getElementById("bookmarksMenu");
+ let BMpopup = document.getElementById("bookmarksMenuPopup");
+ promiseEvent = BrowserTestUtils.waitForEvent(BMpopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(BM, {});
+ await promiseEvent;
+ info("Popupshowing on Bookmarks Menu");
+ menuitem = await locateBookmarkAndTestCtrlClick(BMpopup);
+ ok(BM.open, "Bookmarks Menu's Popup should still be open.");
+
+ // Test Bookmarks Menu (menubar) stayopen clicks: middle-click.
+ promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ EventUtils.synthesizeMouseAtCenter(menuitem, { button: 1 });
+ newTab = await promiseTabOpened;
+ ok(true, "Bookmark middle-click opened new tab.");
+ BrowserTestUtils.removeTab(newTab);
+ ok(BM.open, "Bookmarks Menu's Popup should still be open.");
+
+ // Test Bookmarks Menu (menubar) stayopen clicks: 'Open in new tab' on context menu.
+ newTab = await testContextmenu(menuitem);
+ ok(true, "Bookmark contextmenu opened new tab.");
+ BrowserTestUtils.removeTab(newTab);
+ ok(BM.open, "Bookmarks Menu's Popup should still be open.");
+ promiseEvent = BrowserTestUtils.waitForEvent(BMpopup, "popuphidden");
+ BM.open = false;
+ await promiseEvent;
+
+ // Test Bookmarks Toolbar stayopen clicks - Ctrl-click.
+ let BT = document.getElementById("PlacesToolbarItems");
+ let toolbarbutton = BT.firstElementChild;
+ ok(toolbarbutton, "Folder should be first item on Bookmarks Toolbar.");
+ let buttonMenupopup = toolbarbutton.firstElementChild;
+ Assert.equal(
+ buttonMenupopup.tagName,
+ "menupopup",
+ "Found toolbar button's menupopup."
+ );
+ promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(toolbarbutton, {});
+ await promiseEvent;
+ ok(true, "Bookmarks toolbar folder's popup is open.");
+ menuitem = buttonMenupopup.firstElementChild.nextElementSibling;
+ promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ EventUtils.synthesizeMouseAtCenter(menuitem, { ctrlKey: true });
+ newTab = await promiseTabOpened;
+ ok(
+ true,
+ "Bookmark in folder on bookmark's toolbar ctrl-click opened new tab."
+ );
+ ok(
+ toolbarbutton.open,
+ "Popup of folder on bookmark's toolbar should still be open."
+ );
+ promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popuphidden");
+ toolbarbutton.open = false;
+ await promiseEvent;
+ BrowserTestUtils.removeTab(newTab);
+
+ // Test Bookmarks Toolbar stayopen clicks: middle-click.
+ promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(toolbarbutton, {});
+ await promiseEvent;
+ ok(true, "Bookmarks toolbar folder's popup is open.");
+ promiseTabOpened = BrowserTestUtils.waitForNewTab(gBrowser, null);
+ EventUtils.synthesizeMouseAtCenter(menuitem, { button: 1 });
+ newTab = await promiseTabOpened;
+ ok(
+ true,
+ "Bookmark in folder on Bookmarks Toolbar middle-click opened new tab."
+ );
+ ok(
+ toolbarbutton.open,
+ "Popup of folder on bookmark's toolbar should still be open."
+ );
+ promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popuphidden");
+ toolbarbutton.open = false;
+ await promiseEvent;
+ BrowserTestUtils.removeTab(newTab);
+
+ // Test Bookmarks Toolbar stayopen clicks: 'Open in new tab' on context menu.
+ promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(toolbarbutton, {});
+ await promiseEvent;
+ ok(true, "Bookmarks toolbar folder's popup is open.");
+ newTab = await testContextmenu(menuitem);
+ ok(true, "Bookmark on Bookmarks Toolbar contextmenu opened new tab.");
+ ok(
+ toolbarbutton.open,
+ "Popup of folder on bookmark's toolbar should still be open."
+ );
+ promiseEvent = BrowserTestUtils.waitForEvent(buttonMenupopup, "popuphidden");
+ toolbarbutton.open = false;
+ await promiseEvent;
+ BrowserTestUtils.removeTab(newTab);
+});
diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js b/browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js
new file mode 100644
index 0000000000..9815bd595d
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+const sandbox = sinon.createSandbox();
+
+const URL1 = "https://example.com/1/";
+const URL2 = "https://example.com/2/";
+const BOOKMARKLET_URL = `javascript: (() => {alert('Hello, World!');})();`;
+let bookmarks;
+
+registerCleanupFunction(async function () {
+ sandbox.restore();
+});
+
+add_task(async function test() {
+ // Make sure the bookmarks bar is visible and restore its state on cleanup.
+ let toolbar = document.getElementById("PersonalToolbar");
+ ok(toolbar, "PersonalToolbar should not be null");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ if (toolbar.collapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(function () {
+ return promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+ // Setup the node we will use to be dropped. The actual node used does not
+ // matter because we will set its data, effect, and mimeType manually.
+ let placesItems = document.getElementById("PlacesToolbarItems");
+ Assert.ok(placesItems, "PlacesToolbarItems should not be null");
+
+ /**
+ * Simulates a drop of a bookmarklet URI onto the bookmarks bar.
+ *
+ * @param {string} aEffect
+ * The effect to use for the drop operation: move, copy, or link.
+ */
+ let simulateDragDrop = async function (aEffect) {
+ info("Simulates drag/drop of a new javascript:URL to the bookmarks");
+ await withBookmarksDialog(
+ true,
+ function openDialog() {
+ EventUtils.synthesizeDrop(
+ toolbar,
+ placesItems,
+ [[{ type: "text/x-moz-url", data: BOOKMARKLET_URL }]],
+ aEffect,
+ window
+ );
+ },
+ async function testNameField(dialogWin) {
+ info("Checks that there is a javascript:URL in ShowBookmarksDialog");
+
+ let location = dialogWin.document.getElementById(
+ "editBMPanel_locationField"
+ ).value;
+
+ Assert.equal(
+ location,
+ BOOKMARKLET_URL,
+ "Should have opened the ShowBookmarksDialog with the correct bookmarklet url to be bookmarked"
+ );
+ }
+ );
+
+ info("Simulates drag/drop of a new URL to the bookmarks");
+ let spy = sandbox
+ .stub(PlacesUIUtils, "showBookmarkDialog")
+ .returns(Promise.resolve());
+
+ let promise = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(({ url }) => url == URL1)
+ );
+
+ EventUtils.synthesizeDrop(
+ toolbar,
+ placesItems,
+ [[{ type: "text/x-moz-url", data: URL1 }]],
+ aEffect,
+ window
+ );
+
+ await promise;
+ Assert.ok(spy.notCalled, "ShowBookmarksDialog on drop not called for url");
+ sandbox.restore();
+ };
+
+ let effects = ["copy", "link"];
+ for (let effect of effects) {
+ await simulateDragDrop(effect);
+ }
+
+ info("Move of existing bookmark / bookmarklet on toolbar");
+ // Clean previous bookmarks to ensure right ids count.
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ info("Insert list of bookamrks to have bookmarks (ids) for moving");
+ // Ensure bookmarks are visible on the toolbar.
+ let promiseBookmarksOnToolbar = BrowserTestUtils.waitForMutationCondition(
+ placesItems,
+ { childList: true },
+ () => placesItems.childNodes.length == 3
+ );
+ bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: [
+ {
+ title: "bm1",
+ url: URL1,
+ },
+ {
+ title: "bm2",
+ url: URL2,
+ },
+ {
+ title: "bookmarklet",
+ url: BOOKMARKLET_URL,
+ },
+ ],
+ });
+ await promiseBookmarksOnToolbar;
+
+ let spy = sandbox
+ .stub(PlacesUIUtils, "showBookmarkDialog")
+ .returns(Promise.resolve());
+
+ info("Moving existing Bookmark from position [1] to [0] on Toolbar");
+ let urlMoveNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-moved",
+ events =>
+ events.some(
+ e =>
+ e.parentGuid === PlacesUtils.bookmarks.toolbarGuid &&
+ e.oldParentGuid === PlacesUtils.bookmarks.toolbarGuid &&
+ e.oldIndex == 1 &&
+ e.index == 0
+ )
+ );
+
+ EventUtils.synthesizeDrop(
+ placesItems,
+ placesItems.childNodes[0],
+ [
+ [
+ {
+ type: "text/x-moz-place",
+ data: PlacesUtils.wrapNode(
+ placesItems.childNodes[1]._placesNode,
+ "text/x-moz-place"
+ ),
+ },
+ ],
+ ],
+ "move",
+ window
+ );
+
+ await urlMoveNotification;
+ Assert.ok(spy.notCalled, "ShowBookmarksDialog not called on move for url");
+
+ info("Moving existing Bookmarklet from position [2] to [1] on Toolbar");
+ let bookmarkletMoveNotificatio = PlacesTestUtils.waitForNotification(
+ "bookmark-moved",
+ events =>
+ events.some(
+ e =>
+ e.parentGuid === PlacesUtils.bookmarks.toolbarGuid &&
+ e.oldParentGuid === PlacesUtils.bookmarks.toolbarGuid &&
+ e.oldIndex == 2 &&
+ e.index == 1
+ )
+ );
+
+ EventUtils.synthesizeDrop(
+ toolbar,
+ placesItems.childNodes[1],
+ [
+ [
+ {
+ type: "text/x-moz-place",
+ data: PlacesUtils.wrapNode(
+ placesItems.childNodes[2]._placesNode,
+ "text/x-moz-place"
+ ),
+ },
+ ],
+ ],
+ "move",
+ window
+ );
+
+ await bookmarkletMoveNotificatio;
+ Assert.ok(spy.notCalled, "ShowBookmarksDialog not called on move for url");
+ sandbox.restore();
+});
diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js
new file mode 100644
index 0000000000..1f958989cf
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js
@@ -0,0 +1,69 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check flavors priority when dropping a url/title tuple on the toolbar.
+
+function getDataForType(url, title, type) {
+ switch (type) {
+ case "text/x-moz-url":
+ return `${url}\n${title}`;
+ case "text/plain":
+ return url;
+ case "text/html":
+ return `<a href="${url}">${title}</a>`;
+ }
+ throw new Error("Unknown mime type");
+}
+
+async function testDragDrop(effect, mimeTypes) {
+ const url = "https://www.mozilla.org/drag_drop_test/";
+ const title = "Drag & Drop Test";
+ let promiseItemAddedNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(e => e.url == url)
+ );
+
+ // Ensure there's no bookmark initially
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url });
+ Assert.ok(!bookmark, "There should not be a bookmark to the given URL");
+
+ // We use the toolbar as the drag source, as we just need almost any node
+ // to simulate the drag.
+ let toolbar = document.getElementById("PersonalToolbar");
+ EventUtils.synthesizeDrop(
+ toolbar,
+ document.getElementById("PlacesToolbarItems"),
+ [mimeTypes.map(type => ({ type, data: getDataForType(url, title, type) }))],
+ effect,
+ window
+ );
+
+ await promiseItemAddedNotification;
+
+ // Verify that the drop produces exactly one bookmark.
+ bookmark = await PlacesUtils.bookmarks.fetch({ url });
+ Assert.ok(bookmark, "There should be exactly one bookmark");
+ Assert.equal(bookmark.url, url, "Check bookmark URL is correct");
+ Assert.equal(bookmark.title, title, "Check bookmark title was preserved");
+ await PlacesUtils.bookmarks.remove(bookmark);
+}
+
+add_task(async function test() {
+ registerCleanupFunction(() => PlacesUtils.bookmarks.eraseEverything());
+
+ // Make sure the bookmarks bar is visible and restore its state on cleanup.
+ let toolbar = document.getElementById("PersonalToolbar");
+ if (toolbar.collapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(function () {
+ return promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+
+ // Test a bookmark drop for all of the mime types and effects.
+ let mimeTypes = ["text/plain", "text/html", "text/x-moz-url"];
+ let effects = ["move", "copy", "link"];
+ for (let effect of effects) {
+ await testDragDrop(effect, mimeTypes);
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js
new file mode 100644
index 0000000000..729e456ca0
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test() {
+ // Make sure the bookmarks bar is visible and restore its state on cleanup.
+ let toolbar = document.getElementById("PersonalToolbar");
+ ok(toolbar, "PersonalToolbar should not be null");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ if (toolbar.collapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(function () {
+ return promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+ // Setup the node we will use to be dropped. The actual node used does not
+ // matter because we will set its data, effect, and mimeType manually.
+ let placesItems = document.getElementById("PlacesToolbarItems");
+ Assert.ok(placesItems, "PlacesToolbarItems should not be null");
+ let simulateDragDrop = async function (aEffect, aMimeType) {
+ let urls = [
+ "https://example.com/1/",
+ `javascript: (() => {alert('Hello, World!');})();`,
+ "https://example.com/2/",
+ ];
+
+ let data = urls.map(spec => spec + "\n" + spec).join("\n");
+
+ EventUtils.synthesizeDrop(
+ toolbar,
+ placesItems,
+ [[{ type: aMimeType, data }]],
+ aEffect,
+ window
+ );
+ await PlacesTestUtils.promiseAsyncUpdates();
+ for (let url of urls) {
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url });
+ Assert.ok(!bookmark, "There should be no bookmark");
+ }
+ };
+
+ // Simulate a bookmark drop for all of the mime types and effects.
+ let mimeType = ["text/x-moz-url"];
+ let effects = ["copy", "link"];
+ for (let effect of effects) {
+ await simulateDragDrop(effect, mimeType);
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_toolbar_drop_text.js b/browser/components/places/tests/browser/browser_toolbar_drop_text.js
new file mode 100644
index 0000000000..ba7a7127b0
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbar_drop_text.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+add_task(async function test() {
+ // Make sure the bookmarks bar is visible and restore its state on cleanup.
+ let toolbar = document.getElementById("PersonalToolbar");
+ ok(toolbar, "PersonalToolbar should not be null");
+
+ if (toolbar.collapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(function () {
+ return promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+
+ // Setup the node we will use to be dropped. The actual node used does not
+ // matter because we will set its data, effect, and mimeType manually.
+ let placesItems = document.getElementById("PlacesToolbarItems");
+ ok(placesItems, "PlacesToolbarItems should not be null");
+ Assert.equal(
+ placesItems.localName,
+ "scrollbox",
+ "PlacesToolbarItems should not be null"
+ );
+
+ /**
+ * Simulates a drop of a URI onto the bookmarks bar.
+ *
+ * @param {string} aEffect
+ * The effect to use for the drop operation: move, copy, or link.
+ * @param {string} aMimeType
+ * The mime type to use for the drop operation.
+ */
+ let simulateDragDrop = async function (aEffect, aMimeType) {
+ const url = "http://www.mozilla.org/D1995729-A152-4e30-8329-469B01F30AA7";
+ let promiseItemAddedNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(({ url: eventUrl }) => eventUrl == url)
+ );
+
+ // We use the toolbar as the drag source, as we just need almost any node
+ // to simulate the drag. The actual data for the drop is passed via the
+ // drag data. Note: The toolbar is used rather than another bookmark node,
+ // as we need something that is immovable from a places perspective, as this
+ // forces the move into a copy.
+ EventUtils.synthesizeDrop(
+ toolbar,
+ placesItems,
+ [[{ type: aMimeType, data: url }]],
+ aEffect,
+ window
+ );
+
+ await promiseItemAddedNotification;
+
+ // Verify that the drop produces exactly one bookmark.
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url });
+ Assert.ok(bookmark, "There should be exactly one bookmark");
+
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+
+ // Verify that we removed the bookmark successfully.
+ Assert.equal(
+ await PlacesUtils.bookmarks.fetch({ url }),
+ null,
+ "URI should be removed"
+ );
+ };
+
+ /**
+ * Simulates a drop of multiple URIs onto the bookmarks bar.
+ *
+ * @param {string} aEffect
+ * The effect to use for the drop operation: move, copy, or link.
+ * @param {string} aMimeType
+ * The mime type to use for the drop operation.
+ */
+ let simulateDragDropMultiple = async function (aEffect, aMimeType) {
+ const urls = [
+ "http://www.mozilla.org/C54263C6-A484-46CF-8E2B-FE131586348A",
+ "http://www.mozilla.org/71381257-61E6-4376-AF7C-BF3C5FD8870D",
+ "http://www.mozilla.org/091A88BD-5743-4C16-A005-3D2EA3A3B71E",
+ ];
+ let data;
+ if (aMimeType == "text/x-moz-url") {
+ data = urls.map(spec => spec + "\n" + spec).join("\n");
+ } else {
+ data = urls.join("\n");
+ }
+
+ let promiseItemAddedNotification = PlacesTestUtils.waitForNotification(
+ "bookmark-added",
+ events => events.some(({ url }) => url == urls[2])
+ );
+
+ // See notes for EventUtils.synthesizeDrop in simulateDragDrop().
+ EventUtils.synthesizeDrop(
+ toolbar,
+ placesItems,
+ [[{ type: aMimeType, data }]],
+ aEffect,
+ window
+ );
+
+ await promiseItemAddedNotification;
+
+ // Verify that the drop produces exactly one bookmark per each URL.
+ for (let url of urls) {
+ let bookmark = await PlacesUtils.bookmarks.fetch({ url });
+ Assert.equal(
+ typeof bookmark,
+ "object",
+ "There should be exactly one bookmark"
+ );
+
+ await PlacesUtils.bookmarks.remove(bookmark.guid);
+
+ // Verify that we removed the bookmark successfully.
+ Assert.equal(
+ await PlacesUtils.bookmarks.fetch({ url }),
+ null,
+ "URI should be removed"
+ );
+ }
+ };
+
+ // Simulate a bookmark drop for all of the mime types and effects.
+ let mimeTypes = ["text/plain", "text/x-moz-url"];
+ let effects = ["move", "copy", "link"];
+ for (let effect of effects) {
+ for (let mimeType of mimeTypes) {
+ await simulateDragDrop(effect, mimeType);
+ await simulateDragDropMultiple(effect, mimeType);
+ }
+ }
+});
diff --git a/browser/components/places/tests/browser/browser_toolbar_library_open_recent.js b/browser/components/places/tests/browser/browser_toolbar_library_open_recent.js
new file mode 100644
index 0000000000..5bedd02a83
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbar_library_open_recent.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Tests that recently added bookmarks can be opened.
+ */
+
+const BASE_URL =
+ "http://example.org/browser/browser/components/places/tests/browser/";
+const bookmarkItems = [
+ {
+ url: `${BASE_URL}bookmark_dummy_1.html`,
+ title: "Custom Title 1",
+ },
+ {
+ url: `${BASE_URL}bookmark_dummy_2.html`,
+ title: "Custom Title 2",
+ },
+];
+let openedTabs = [];
+
+async function openBookmarksPanelInLibraryToolbarButton() {
+ let libraryBtn = document.getElementById("library-button");
+ libraryBtn.click();
+ let libView = document.getElementById("appMenu-libraryView");
+ let viewShownPromise = BrowserTestUtils.waitForEvent(libView, "ViewShown");
+ await viewShownPromise;
+
+ let bookmarksButton;
+ await TestUtils.waitForCondition(() => {
+ bookmarksButton = document.getElementById(
+ "appMenu-library-bookmarks-button"
+ );
+ return bookmarksButton;
+ }, "Should have the library bookmarks button");
+ bookmarksButton.click();
+
+ let BookmarksView = document.getElementById("PanelUI-bookmarks");
+ let viewRecentPromise = BrowserTestUtils.waitForEvent(
+ BookmarksView,
+ "ViewShown"
+ );
+ await viewRecentPromise;
+}
+
+async function openBookmarkedItemInNewTab(itemFromMenu) {
+ let placesContext = document.getElementById("placesContext");
+ let openContextMenuPromise = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(itemFromMenu, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await openContextMenuPromise;
+ info("Opened context menu");
+
+ let tabCreatedPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ let openInNewTabOption = document.getElementById("placesContext_open:newtab");
+ placesContext.activateItem(openInNewTabOption);
+ info("Click open in new tab");
+
+ let lastOpenedTab = await tabCreatedPromise;
+ Assert.equal(
+ lastOpenedTab.linkedBrowser.currentURI.spec,
+ itemFromMenu._placesNode.uri,
+ "Should have opened the correct URI"
+ );
+ openedTabs.push(lastOpenedTab);
+}
+
+async function closeLibraryMenu() {
+ let libView = document.getElementById("appMenu-libraryView");
+ let viewHiddenPromise = BrowserTestUtils.waitForEvent(libView, "ViewHiding");
+ EventUtils.synthesizeKey("KEY_Escape", {}, window);
+ await viewHiddenPromise;
+}
+
+async function closeTabs() {
+ for (var i = 0; i < openedTabs.length; i++) {
+ await gBrowser.removeTab(openedTabs[i]);
+ }
+ Assert.equal(gBrowser.tabs.length, 1, "Should close all opened tabs");
+}
+
+async function getRecentlyBookmarkedItems() {
+ let historyMenu = document.getElementById("panelMenu_bookmarksMenu");
+ let items = historyMenu.querySelectorAll("toolbarbutton");
+ Assert.ok(items, "Recently bookmarked items should exists");
+
+ await TestUtils.waitForCondition(
+ () => items[0].attributes !== "undefined",
+ "Custom bookmark exists"
+ );
+
+ if (items) {
+ Assert.equal(
+ items[0]._placesNode.uri,
+ bookmarkItems[1].url,
+ "Should match the expected url"
+ );
+ Assert.equal(
+ items[0].getAttribute("label"),
+ bookmarkItems[1].title,
+ "Should be the expected title"
+ );
+ Assert.equal(
+ items[1]._placesNode.uri,
+ bookmarkItems[0].url,
+ "Should match the expected url"
+ );
+ Assert.equal(
+ items[1].getAttribute("label"),
+ bookmarkItems[0].title,
+ "Should be the expected title"
+ );
+ }
+ return Array.from(items).slice(0, 2);
+}
+
+add_setup(async function () {
+ let libraryButton = CustomizableUI.getPlacementOfWidget("library-button");
+ if (!libraryButton) {
+ CustomizableUI.addWidgetToArea(
+ "library-button",
+ CustomizableUI.AREA_NAVBAR
+ );
+ }
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.menuGuid,
+ children: bookmarkItems,
+ });
+
+ registerCleanupFunction(async () => {
+ await PlacesUtils.bookmarks.eraseEverything();
+ CustomizableUI.reset();
+ });
+});
+
+add_task(async function test_recently_added() {
+ await openBookmarksPanelInLibraryToolbarButton();
+
+ let historyItems = await getRecentlyBookmarkedItems();
+
+ for (let item of historyItems) {
+ await openBookmarkedItemInNewTab(item);
+ }
+
+ await closeLibraryMenu();
+
+ registerCleanupFunction(async () => {
+ await closeTabs();
+ });
+});
diff --git a/browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js b/browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js
new file mode 100644
index 0000000000..89171deb1f
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js
@@ -0,0 +1,601 @@
+/* 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/. */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+"use strict";
+
+const bookmarksInfo = [
+ {
+ title: "firefox",
+ url: "http://example.com",
+ },
+ {
+ title: "rules",
+ url: "http://example.com/2",
+ },
+ {
+ title: "yo",
+ url: "http://example.com/2",
+ },
+];
+
+/**
+ * Test showing the "Other Bookmarks" folder in the bookmarks toolbar.
+ */
+
+// Setup.
+add_setup(async function () {
+ // Disable window occlusion. See bug 1733955 / bug 1779559.
+ if (navigator.platform.indexOf("Win") == 0) {
+ await SpecialPowers.pushPrefEnv({
+ set: [["widget.windows.window_occlusion_tracking.enabled", false]],
+ });
+ }
+
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+
+ await setupBookmarksToolbar();
+
+ // Cleanup.
+ registerCleanupFunction(async () => {
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+// Test the "Other Bookmarks" folder is shown in the toolbar when
+// bookmarks are stored under that folder.
+add_task(async function testShowingOtherBookmarksInToolbar() {
+ info("Check the initial state of the Other Bookmarks folder.");
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ await setupBookmarksToolbar(win);
+ ok(
+ !win.document.getElementById("OtherBookmarks"),
+ "Shouldn't have an Other Bookmarks button."
+ );
+ await BrowserTestUtils.closeWindow(win);
+
+ info("Check visibility of an empty Other Bookmarks folder.");
+ await testIsOtherBookmarksHidden(true);
+
+ info("Ensure folder appears in toolbar when a new bookmark is added.");
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: bookmarksInfo,
+ });
+ await testIsOtherBookmarksHidden(false);
+
+ info("Ensure folder disappears from toolbar when no bookmarks are present.");
+ await PlacesUtils.bookmarks.remove(bookmarks);
+ await testIsOtherBookmarksHidden(true);
+});
+
+// Test that folder visibility is correct when moving bookmarks to an empty
+// "Other Bookmarks" folder and vice versa.
+add_task(async function testOtherBookmarksVisibilityWhenMovingBookmarks() {
+ info("Add bookmarks to Bookmarks Toolbar.");
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: bookmarksInfo,
+ });
+ await testIsOtherBookmarksHidden(true);
+
+ info("Move toolbar bookmarks to Other Bookmarks folder.");
+ await PlacesUtils.bookmarks.moveToFolder(
+ bookmarks.map(b => b.guid),
+ PlacesUtils.bookmarks.unfiledGuid,
+ PlacesUtils.bookmarks.DEFAULT_INDEX
+ );
+ await testIsOtherBookmarksHidden(false);
+
+ info("Move bookmarks from Other Bookmarks back to the toolbar.");
+ await PlacesUtils.bookmarks.moveToFolder(
+ bookmarks.map(b => b.guid),
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.DEFAULT_INDEX
+ );
+ await testIsOtherBookmarksHidden(true);
+});
+
+// Test OtherBookmarksPopup in toolbar.
+add_task(async function testOtherBookmarksMenuPopup() {
+ info("Add bookmarks to Other Bookmarks folder.");
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: bookmarksInfo,
+ });
+
+ await testIsOtherBookmarksHidden(false);
+
+ info("Check the popup menu has correct number of children.");
+ await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks");
+ testNumberOfMenuPopupChildren("#OtherBookmarksPopup", 3);
+ await closeMenuPopup("#OtherBookmarksPopup");
+
+ info("Remove a bookmark.");
+ await PlacesUtils.bookmarks.remove(bookmarks[0]);
+
+ await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks");
+ testNumberOfMenuPopupChildren("#OtherBookmarksPopup", 2);
+ await closeMenuPopup("#OtherBookmarksPopup");
+});
+
+// Test that folders in the Other Bookmarks folder expand
+add_task(async function testFolderPopup() {
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ children: [
+ {
+ title: "example",
+ url: "http://example.com/3",
+ },
+ ],
+ },
+ ],
+ });
+
+ info("Check for popup showing event when folder menuitem is selected.");
+ await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks");
+ await openMenuPopup(
+ "#OtherBookmarksPopup menu menupopup",
+ "#OtherBookmarksPopup menu"
+ );
+ ok(true, "Folder menu stored in Other Bookmarks expands.");
+ testNumberOfMenuPopupChildren("#OtherBookmarksPopup menu menupopup", 1);
+ await closeMenuPopup("#OtherBookmarksPopup");
+});
+
+add_task(async function testOnlyShowOtherFolderInBookmarksToolbar() {
+ await setupBookmarksToolbar();
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: bookmarksInfo,
+ });
+
+ await testIsOtherBookmarksHidden(false);
+
+ // Test that moving the personal-bookmarks widget out of the
+ // Bookmarks Toolbar will hide the "Other Bookmarks" folder.
+ let widgetId = "personal-bookmarks";
+ CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR);
+ await testIsOtherBookmarksHidden(true);
+
+ CustomizableUI.reset();
+ await testIsOtherBookmarksHidden(false);
+});
+
+// Test that the menu popup updates when menu items are deleted from it while
+// it's open.
+add_task(async function testDeletingMenuItems() {
+ await setupBookmarksToolbar();
+
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: bookmarksInfo,
+ });
+
+ await testIsOtherBookmarksHidden(false);
+
+ await openMenuPopup("#OtherBookmarksPopup", "#OtherBookmarks");
+ testNumberOfMenuPopupChildren("#OtherBookmarksPopup", 3);
+
+ info("Open context menu for popup.");
+ let placesContext = document.getElementById("placesContext");
+ let popupEventPromise = BrowserTestUtils.waitForPopupEvent(
+ placesContext,
+ "shown"
+ );
+ let menuitem = document.querySelector("#OtherBookmarksPopup menuitem");
+ EventUtils.synthesizeMouseAtCenter(menuitem, { type: "contextmenu" });
+ await popupEventPromise;
+
+ info("Delete bookmark menu item from popup.");
+ let deleteMenuBookmark = document.getElementById(
+ "placesContext_deleteBookmark"
+ );
+ placesContext.activateItem(deleteMenuBookmark);
+
+ await TestUtils.waitForCondition(() => {
+ let popup = document.querySelector("#OtherBookmarksPopup");
+ let items = popup.querySelectorAll("menuitem");
+ return items.length === 2;
+ }, "Failed to delete bookmark menuitem. Expected 2 menu items after deletion.");
+ ok(true, "Menu item was removed from the popup.");
+ await closeMenuPopup("#OtherBookmarksPopup");
+});
+
+add_task(async function no_errors_when_bookmarks_placed_in_palette() {
+ CustomizableUI.removeWidgetFromArea("personal-bookmarks");
+
+ let consoleErrors = 0;
+
+ let errorListener = {
+ observe(error) {
+ ok(false, `${error.message}, ${error.stack}, ${JSON.stringify(error)}`);
+ consoleErrors++;
+ },
+ };
+ Services.console.registerListener(errorListener);
+
+ let bookmarks = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: bookmarksInfo,
+ });
+ is(consoleErrors, 0, "There should be no console errors");
+
+ Services.console.unregisterListener(errorListener);
+ await PlacesUtils.bookmarks.remove(bookmarks);
+ CustomizableUI.reset();
+});
+
+// Test "Show Other Bookmarks" menu item visibility in toolbar context menu.
+add_task(async function testShowingOtherBookmarksContextMenuItem() {
+ await setupBookmarksToolbar();
+
+ info("Add bookmark to Other Bookmarks.");
+ let bookmark = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [{ title: "firefox", url: "http://example.com" }],
+ });
+
+ info("'Show Other Bookmarks' menu item should be checked by default.");
+ await testOtherBookmarksCheckedState(true);
+
+ info("Toggle off showing the Other Bookmarks folder.");
+ await selectShowOtherBookmarksMenuItem();
+ await testOtherBookmarksCheckedState(false);
+ await testIsOtherBookmarksHidden(true);
+
+ info("Toggle on showing the Other Bookmarks folder.");
+ await selectShowOtherBookmarksMenuItem();
+ await testOtherBookmarksCheckedState(true);
+ await testIsOtherBookmarksHidden(false);
+
+ info(
+ "Ensure 'Show Other Bookmarks' isn't shown when Other Bookmarks is empty."
+ );
+ await PlacesUtils.bookmarks.remove(bookmark);
+ await testIsOtherBookmarksMenuItemEnabled(false);
+
+ info("Add a bookmark to the empty Other Bookmarks folder.");
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [{ title: "firefox", url: "http://example.com" }],
+ });
+ await testIsOtherBookmarksMenuItemEnabled(true);
+
+ info(
+ "Ensure that displaying Other Bookmarks is consistent across separate windows."
+ );
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ await TestUtils.waitForCondition(() => {
+ let otherBookmarks = newWin.document.getElementById("OtherBookmarks");
+ return otherBookmarks && !otherBookmarks.hidden;
+ }, "Other Bookmarks folder failed to show in other window.");
+
+ info("Hide the Other Bookmarks folder from the original window.");
+ await selectShowOtherBookmarksMenuItem();
+
+ await TestUtils.waitForCondition(() => {
+ let otherBookmarks = newWin.document.getElementById("OtherBookmarks");
+ return !otherBookmarks || otherBookmarks.hidden;
+ }, "Other Bookmarks folder failed to be hidden in other window.");
+ ok(true, "Other Bookmarks was successfully hidden in other window.");
+
+ info("Show the Other Bookmarks folder from the original window.");
+ await selectShowOtherBookmarksMenuItem();
+
+ await TestUtils.waitForCondition(() => {
+ let otherBookmarks = newWin.document.getElementById("OtherBookmarks");
+ return otherBookmarks && !otherBookmarks.hidden;
+ }, "Other Bookmarks folder failed to be shown in other window.");
+ ok(true, "Other Bookmarks was successfully shown in other window.");
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+// Test 'Show Other Bookmarks' isn't shown when pref is false.
+add_task(async function showOtherBookmarksMenuItemPrefDisabled() {
+ await setupBookmarksToolbar();
+ await testIsOtherBookmarksMenuItemEnabled(false);
+});
+
+// Test that node visibility for toolbar overflow is consisten when the "Other Bookmarks"
+// folder is shown/hidden.
+add_task(async function testOtherBookmarksToolbarOverFlow() {
+ await setupBookmarksToolbar();
+
+ info(
+ "Ensure that visible nodes when showing/hiding Other Bookmarks is consistent across separate windows."
+ );
+ // Add bookmarks to other bookmarks
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [{ title: "firefox", url: "http://example.com" }],
+ });
+
+ let newWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Add bookmarks to the toolbar
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: Array(100)
+ .fill("")
+ .map((_, i) => ({ title: `test ${i}`, url: `http:example.com/${i}` })),
+ });
+
+ info("Hide the Other Bookmarks folder from the original window.");
+ await selectShowOtherBookmarksMenuItem("#OtherBookmarks");
+ await BrowserTestUtils.waitForEvent(
+ document.getElementById("PersonalToolbar"),
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ ok(true, "Nodes successfully updated for both windows.");
+ await testUpdatedNodeVisibility(newWin);
+
+ info("Show the Other Bookmarks folder from the original window.");
+ await selectShowOtherBookmarksMenuItem("#PlacesChevron");
+ await BrowserTestUtils.waitForEvent(
+ document.getElementById("PersonalToolbar"),
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ ok(true, "Nodes successfully updated for both windows.");
+ await testUpdatedNodeVisibility(newWin);
+
+ await BrowserTestUtils.closeWindow(newWin);
+});
+
+/**
+ * Tests whether or not the "Other Bookmarks" folder is visible.
+ *
+ * @param {boolean} expected
+ * The expected state of the Other Bookmarks folder. There are 3:
+ * - the folder node isn't initialized and is therefore not visible,
+ * - the folder node is initialized and is hidden
+ * - the folder node is initialized and is visible
+ */
+async function testIsOtherBookmarksHidden(expected) {
+ info("Test whether or not the 'Other Bookmarks' folder is visible.");
+
+ // Ensure the toolbar is visible.
+ let toolbar = document.getElementById("PersonalToolbar");
+ await promiseSetToolbarVisibility(toolbar, true);
+
+ let otherBookmarks = document.getElementById("OtherBookmarks");
+
+ await TestUtils.waitForCondition(() => {
+ otherBookmarks = document.getElementById("OtherBookmarks");
+ let isHidden = !otherBookmarks || otherBookmarks.hidden;
+ return isHidden === expected;
+ }, "Other Bookmarks folder failed to change hidden state.");
+
+ ok(true, `Other Bookmarks folder "hidden" state should be ${expected}.`);
+}
+
+/**
+ * Tests number of menu items in Other Bookmarks popup.
+ *
+ * @param {string} selector
+ * The selector for getting the menupopup element we want to test.
+ * @param {number} expected
+ * The expected number of menuitem elements inside the menupopup.
+ */
+function testNumberOfMenuPopupChildren(selector, expected) {
+ let popup = document.querySelector(selector);
+ let items = popup.querySelectorAll("menuitem");
+
+ is(
+ items.length,
+ expected,
+ `Number of menu items for ${selector} should be ${expected}.`
+ );
+}
+
+/**
+ * Test helper for checking the 'checked' state of the "Show Other Bookmarks" menu item
+ * after selecting it from the context menu.
+ *
+ * @param {boolean} expectedCheckedState
+ * Whether or not the menu item is checked.
+ */
+async function testOtherBookmarksCheckedState(expectedCheckedState) {
+ info("Check 'Show Other Bookmarks' menu item state");
+ await openToolbarContextMenu();
+
+ let otherBookmarksMenuItem = document.querySelector(
+ "#show-other-bookmarks_PersonalToolbar"
+ );
+
+ is(
+ otherBookmarksMenuItem.getAttribute("checked"),
+ `${expectedCheckedState}`,
+ `Other Bookmarks item's checked state should be ${expectedCheckedState}`
+ );
+
+ await closeToolbarContextMenu();
+}
+
+/**
+ * Test helper for checking whether or not the 'Show Other Bookmarks' menu item
+ * is enabled in the toolbar's context menu.
+ *
+ * @param {boolean} expected
+ * Whether or not the menu item is enabled in the toolbar conext menu.
+ */
+async function testIsOtherBookmarksMenuItemEnabled(expected) {
+ await openToolbarContextMenu();
+
+ let otherBookmarksMenuItem = document.querySelector(
+ "#show-other-bookmarks_PersonalToolbar"
+ );
+
+ is(
+ !otherBookmarksMenuItem.disabled,
+ expected,
+ "'Show Other Bookmarks' menu item appearance state is correct."
+ );
+
+ await closeToolbarContextMenu();
+}
+
+/**
+ * Helper for opening a menu popup.
+ *
+ * @param {string} popupSelector
+ * The selector for the menupopup element we want to open.
+ * @param {string} targetSelector
+ * The selector for the element with the popup showing event.
+ */
+async function openMenuPopup(popupSelector, targetSelector) {
+ let popup = document.querySelector(popupSelector);
+ let target = document.querySelector(targetSelector);
+
+ EventUtils.synthesizeMouseAtCenter(target, {});
+
+ await BrowserTestUtils.waitForPopupEvent(popup, "shown");
+}
+
+/**
+ * Helper for closing a menu popup.
+ *
+ * @param {string} popupSelector
+ * The selector for the menupopup element we want to close.
+ */
+async function closeMenuPopup(popupSelector) {
+ let popup = document.querySelector(popupSelector);
+
+ info("Closing menu popup.");
+ popup.hidePopup();
+ await BrowserTestUtils.waitForPopupEvent(popup, "hidden");
+}
+
+/**
+ * Helper for opening the toolbar context menu.
+ *
+ * @param {string} toolbarSelector
+ * Optional. The selector for the toolbar context menu.
+ * Defaults to #PlacesToolbarItems.
+ */
+async function openToolbarContextMenu(toolbarSelector = "#PlacesToolbarItems") {
+ let contextMenu = document.getElementById("placesContext");
+ let toolbar = document.querySelector(toolbarSelector);
+ let openToolbarContextMenuPromise = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "shown"
+ );
+
+ // Use the end of the toolbar because the beginning (and even middle, on
+ // some resolutions) might be occluded by the empty toolbar message, which
+ // has a different context menu.
+ let bounds = toolbar.getBoundingClientRect();
+ EventUtils.synthesizeMouse(toolbar, bounds.width - 5, 5, {
+ type: "contextmenu",
+ });
+
+ await openToolbarContextMenuPromise;
+}
+
+/**
+ * Helper for closing the toolbar context menu.
+ */
+async function closeToolbarContextMenu() {
+ let contextMenu = document.getElementById("placesContext");
+ let closeToolbarContextMenuPromise = BrowserTestUtils.waitForPopupEvent(
+ contextMenu,
+ "hidden"
+ );
+ contextMenu.hidePopup();
+ await closeToolbarContextMenuPromise;
+}
+
+/**
+ * Helper for setting up the bookmarks toolbar state. This ensures the beginning
+ * of a task will always have the bookmark toolbar in a state that makes the
+ * Other Bookmarks folder testable.
+ *
+ * @param {object} [win]
+ * The window object to use.
+ */
+async function setupBookmarksToolbar(win = window) {
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ await PlacesUtils.bookmarks.eraseEverything();
+}
+
+/**
+ * Helper for selecting the "Show Other Bookmarks" menu item from the bookmarks
+ * toolbar context menu.
+ *
+ * @param {string} selector
+ * Optional. The selector for the node that triggers showing the
+ * "Show Other Bookmarks" context menu item in the toolbar.
+ * Defaults to #PlacesToolbarItem when `openToolbarContextMenu` is
+ * called.
+ */
+async function selectShowOtherBookmarksMenuItem(selector) {
+ info("Select 'Show Other Bookmarks' menu item");
+ await openToolbarContextMenu(selector);
+
+ let otherBookmarksMenuItem = document.querySelector(
+ "#show-other-bookmarks_PersonalToolbar"
+ );
+ let contextMenu = document.getElementById("placesContext");
+
+ contextMenu.activateItem(otherBookmarksMenuItem);
+
+ await BrowserTestUtils.waitForPopupEvent(contextMenu, "hidden");
+ await closeToolbarContextMenu();
+}
+
+/**
+ * Test helper for node visibility in the bookmarks toolbar between two windows.
+ *
+ * @param {Window} otherWin
+ * The other window whose toolbar items we want to compare with.
+ */
+function testUpdatedNodeVisibility(otherWin) {
+ // Get visible toolbar nodes for both the current and other windows.
+ let toolbarItems = document.getElementById("PlacesToolbarItems");
+ let currentVisibleNodes = [];
+
+ for (let node of toolbarItems.children) {
+ if (node.style.visibility === "visible") {
+ currentVisibleNodes.push(node);
+ }
+ }
+
+ let otherToolbarItems =
+ otherWin.document.getElementById("PlacesToolbarItems");
+ let otherVisibleNodes = [];
+
+ for (let node of otherToolbarItems.children) {
+ if (node.style.visibility === "visible") {
+ otherVisibleNodes.push(node);
+ }
+ }
+
+ let lastIdx = otherVisibleNodes.length - 1;
+
+ is(
+ currentVisibleNodes[lastIdx]?.bookmarkGuid,
+ otherVisibleNodes[lastIdx]?.bookmarkGuid,
+ "Last visible toolbar bookmark is the same in both windows."
+ );
+}
diff --git a/browser/components/places/tests/browser/browser_toolbar_overflow.js b/browser/components/places/tests/browser/browser_toolbar_overflow.js
new file mode 100644
index 0000000000..3f16c2a126
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbar_overflow.js
@@ -0,0 +1,436 @@
+/* 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 the bookmarks toolbar overflow.
+ */
+
+var gToolbar = document.getElementById("PersonalToolbar");
+var gChevron = document.getElementById("PlacesChevron");
+
+const BOOKMARKS_COUNT = 250;
+
+add_setup(async function () {
+ let wasCollapsed = gToolbar.collapsed;
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Add bookmarks.
+ await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.toolbarGuid,
+ children: Array(BOOKMARKS_COUNT)
+ .fill("")
+ .map((_, i) => ({ url: `http://test.places.y${i}/` })),
+ });
+
+ // Toggle the bookmarks toolbar so that we start from a stable situation and
+ // are not affected by all bookmarks removal.
+ await toggleToolbar(false);
+ await toggleToolbar(true);
+
+ registerCleanupFunction(async () => {
+ if (wasCollapsed) {
+ await toggleToolbar(false);
+ }
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.history.clear();
+ });
+});
+
+add_task(async function test_overflow() {
+ // Check that the overflow chevron is visible.
+ Assert.ok(!gChevron.collapsed, "The overflow chevron should be visible");
+ let children = getPlacesChildren();
+ Assert.ok(
+ children.length < BOOKMARKS_COUNT,
+ "Not all the nodes should be built by default"
+ );
+ let visibleNodes = [];
+ for (let node of children) {
+ if (getComputedStyle(node).visibility == "visible") {
+ visibleNodes.push(node);
+ }
+ }
+ Assert.ok(
+ visibleNodes.length < children.length,
+ `The number of visible nodes (${visibleNodes.length}) should be smaller than the number of built nodes (${children.length})`
+ );
+
+ await test_index(
+ "Node at the last visible index",
+ visibleNodes.length - 1,
+ "visible"
+ );
+ await test_index(
+ "Node at the first invisible index",
+ visibleNodes.length,
+ "hidden"
+ );
+ await test_index("First non-built node", children.length, undefined);
+ await test_index("Later non-built node", children.length + 1, undefined);
+
+ await test_move_index(
+ "Move node from last visible to first hidden",
+ visibleNodes.length - 1,
+ visibleNodes.length,
+ "visible",
+ "hidden"
+ );
+ await test_move_index(
+ "Move node from fist visible to last built",
+ 0,
+ children.length - 1,
+ "visible",
+ "hidden"
+ );
+ await test_move_index(
+ "Move node from fist visible to first non built",
+ 0,
+ children.length,
+ "visible",
+ undefined
+ );
+});
+
+add_task(async function test_separator_first() {
+ await toggleToolbar(false);
+ // Check that if a separator is the first node, we still calculate overflow
+ // properly.
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: 0,
+ });
+ await toggleToolbar(true, 2);
+
+ let children = getPlacesChildren();
+ Assert.greater(children.length, 2, "Multiple elements are visible");
+ Assert.equal(
+ children[1]._placesNode.uri,
+ "http://test.places.y0/",
+ "Found the first bookmark"
+ );
+ Assert.equal(
+ getComputedStyle(children[1]).visibility,
+ "visible",
+ "The first bookmark is visible"
+ );
+
+ await PlacesUtils.bookmarks.remove(bm);
+});
+
+add_task(async function test_newWindow_noOverflow() {
+ info(
+ "Check toolbar in a new widow when it was already visible and not overflowed"
+ );
+ Assert.ok(!gToolbar.collapsed, "Toolbar is not collapsed in original window");
+ await PlacesUtils.bookmarks.eraseEverything();
+ // Add a single bookmark.
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://toolbar.overflow/",
+ title: "Example",
+ });
+ // Add a favicon for the bookmark.
+ let favicon =
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAA" +
+ "AAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==";
+ await PlacesTestUtils.addFavicons(
+ new Map([["http://toolbar.overflow/", favicon]])
+ );
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ try {
+ let toolbar = win.document.getElementById("PersonalToolbar");
+ Assert.ok(!toolbar.collapsed, "The toolbar is not collapsed");
+ let content = win.document.getElementById("PlacesToolbarItems");
+ await TestUtils.waitForCondition(() => {
+ return (
+ content.children.length == 1 &&
+ content.children[0].hasAttribute("image")
+ );
+ });
+ let chevron = win.document.getElementById("PlacesChevron");
+ Assert.ok(chevron.collapsed, "The chevron should be collapsed");
+ } finally {
+ await BrowserTestUtils.closeWindow(win);
+ }
+});
+
+async function test_index(desc, index, expected) {
+ info(desc);
+ let children = getPlacesChildren();
+ let originalLen = children.length;
+ let nodeExisted = children.length > index;
+ let previousNodeIsVisible =
+ nodeExisted &&
+ getComputedStyle(children[index - 1]).visibility == "visible";
+ let promise = promiseUpdateVisibility(
+ expected == "visible" || previousNodeIsVisible
+ );
+
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: "http://test.places.added/",
+ index,
+ });
+ Assert.equal(bm.index, index, "Sanity check the bookmark index");
+ await promise;
+ children = getPlacesChildren();
+
+ if (!expected) {
+ Assert.ok(
+ children.length <= index,
+ "The new node should not have been added"
+ );
+ } else {
+ Assert.equal(
+ children[index]._placesNode.bookmarkGuid,
+ bm.guid,
+ "Found the added bookmark at the expected position"
+ );
+ Assert.equal(
+ getComputedStyle(children[index]).visibility,
+ expected,
+ `The bookmark node should be ${expected}`
+ );
+ }
+ Assert.equal(
+ children.length,
+ originalLen,
+ "Number of built nodes should stay the same"
+ );
+
+ info("Remove the node");
+ promise = promiseUpdateVisibility(expected == "visible");
+
+ await PlacesUtils.bookmarks.remove(bm);
+ await promise;
+ children = getPlacesChildren();
+
+ if (expected && nodeExisted) {
+ Assert.equal(
+ children[index]._placesNode.uri,
+ `http://test.places.y${index}/`,
+ "Found the previous bookmark at the expected position"
+ );
+ Assert.equal(
+ getComputedStyle(children[index]).visibility,
+ expected,
+ `The bookmark node should be ${expected}`
+ );
+ }
+ Assert.equal(
+ children.length,
+ originalLen,
+ "Number of built nodes should stay the same"
+ );
+}
+
+async function test_move_index(desc, fromIndex, toIndex, original, expected) {
+ info(desc);
+ let children = getPlacesChildren();
+ let originalLen = children.length;
+ let movedGuid = children[fromIndex]._placesNode.bookmarkGuid;
+ let existingGuid = children[toIndex]
+ ? children[toIndex]._placesNode.bookmarkGuid
+ : null;
+ let existingIndex = fromIndex < toIndex ? toIndex - 1 : toIndex + 1;
+
+ Assert.equal(
+ getComputedStyle(children[fromIndex]).visibility,
+ original,
+ `The bookmark node should be ${original}`
+ );
+ let promise = promiseUpdateVisibility(
+ original == "visible" || expected == "visible"
+ );
+
+ await PlacesUtils.bookmarks.update({
+ guid: movedGuid,
+ index: toIndex,
+ });
+ await promise;
+ children = getPlacesChildren();
+
+ if (!expected) {
+ Assert.ok(
+ children.length <= toIndex,
+ "Node in the new position is not expected"
+ );
+ Assert.ok(
+ children[originalLen - 1],
+ "We should keep number of built nodes consistent"
+ );
+ } else {
+ Assert.equal(
+ children[toIndex]._placesNode.bookmarkGuid,
+ movedGuid,
+ "Found the moved bookmark at the expected position"
+ );
+ Assert.equal(
+ getComputedStyle(children[toIndex]).visibility,
+ expected,
+ `The destination bookmark node should be ${expected}`
+ );
+ }
+ Assert.equal(
+ getComputedStyle(children[fromIndex]).visibility,
+ original,
+ `The origin bookmark node should be ${original}`
+ );
+ if (existingGuid) {
+ Assert.equal(
+ children[existingIndex]._placesNode.bookmarkGuid,
+ existingGuid,
+ "Found the pushed away bookmark at the expected position"
+ );
+ }
+ Assert.equal(
+ children.length,
+ originalLen,
+ "Number of built nodes should stay the same"
+ );
+
+ info("Moving back the node");
+ promise = promiseUpdateVisibility(
+ original == "visible" || expected == "visible"
+ );
+
+ await PlacesUtils.bookmarks.update({
+ guid: movedGuid,
+ index: fromIndex,
+ });
+ await promise;
+ children = getPlacesChildren();
+
+ Assert.equal(
+ children[fromIndex]._placesNode.bookmarkGuid,
+ movedGuid,
+ "Found the moved bookmark at the expected position"
+ );
+ if (expected) {
+ Assert.equal(
+ getComputedStyle(children[toIndex]).visibility,
+ expected,
+ `The bookmark node should be ${expected}`
+ );
+ }
+ Assert.equal(
+ getComputedStyle(children[fromIndex]).visibility,
+ original,
+ `The bookmark node should be ${original}`
+ );
+ if (existingGuid) {
+ Assert.equal(
+ children[toIndex]._placesNode.bookmarkGuid,
+ existingGuid,
+ "Found the pushed away bookmark at the expected position"
+ );
+ }
+ Assert.equal(
+ children.length,
+ originalLen,
+ "Number of built nodes should stay the same"
+ );
+}
+
+add_task(async function test_separator_first() {
+ // Check that if there are only separators, we still show nodes properly.
+ await toggleToolbar(false);
+ await PlacesUtils.bookmarks.eraseEverything();
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: 0,
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ index: 0,
+ });
+ await toggleToolbar(true, 2);
+
+ let children = getPlacesChildren();
+ Assert.equal(children.length, 2, "The expected elements are visible");
+ Assert.equal(
+ getComputedStyle(children[0]).visibility,
+ "visible",
+ "The first bookmark is visible"
+ );
+ Assert.equal(
+ getComputedStyle(children[1]).visibility,
+ "visible",
+ "The second bookmark is visible"
+ );
+
+ await toggleToolbar(false);
+ await PlacesUtils.bookmarks.eraseEverything();
+});
+
+/**
+ * If the passed-in condition is fulfilled, awaits for the toolbar nodes
+ * visibility to have been updated.
+ *
+ * @param {boolean} [condition] Awaits for visibility only if this condition is true.
+ * @returns {Promise} resolved when the condition is not fulfilled or the
+ * visilibily update happened.
+ */
+function promiseUpdateVisibility(condition = true) {
+ if (condition) {
+ return BrowserTestUtils.waitForEvent(
+ gToolbar,
+ "BookmarksToolbarVisibilityUpdated"
+ );
+ }
+ return Promise.resolve();
+}
+
+/**
+ * Returns an array of toolbar children that are Places nodes, ignoring things
+ * like the chevron or other additional buttons.
+ *
+ * @returns {Array} An array of Places element nodes.
+ */
+function getPlacesChildren() {
+ return Array.prototype.filter.call(
+ document.getElementById("PlacesToolbarItems").children,
+ c => c._placesNode?.itemId
+ );
+}
+
+/**
+ * Toggles the toolbar on or off.
+ *
+ * @param {boolean} show Whether to show or hide the toolbar.
+ * @param {number} [expectedMinChildCount] Optional number of Places nodes that
+ * should be visible on the toolbar.
+ */
+async function toggleToolbar(show, expectedMinChildCount = 0) {
+ let promiseReady = Promise.resolve();
+ if (show) {
+ promiseReady = promiseUpdateVisibility();
+ }
+
+ await promiseSetToolbarVisibility(gToolbar, show);
+ await promiseReady;
+
+ if (show) {
+ if (getPlacesChildren().length < expectedMinChildCount) {
+ await new Promise(resolve => {
+ info("Waiting for bookmark elements to appear");
+ let mut = new MutationObserver(mutations => {
+ let children = getPlacesChildren();
+ info(`${children.length} bookmark elements appeared`);
+ if (children.length >= expectedMinChildCount) {
+ resolve();
+ mut.disconnect();
+ }
+ });
+ mut.observe(document.getElementById("PlacesToolbarItems"), {
+ childList: true,
+ });
+ });
+ }
+ }
+}
diff --git a/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js
new file mode 100644
index 0000000000..a87f26caab
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js
@@ -0,0 +1,72 @@
+CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ 4
+);
+var bookmarksMenuButton = document.getElementById("bookmarks-menu-button");
+var BMB_menuPopup = document.getElementById("BMB_bookmarksPopup");
+var BMB_showAllBookmarks = document.getElementById("BMB_bookmarksShowAll");
+var contextMenu = document.getElementById("placesContext");
+var newBookmarkItem = document.getElementById("placesContext_new:bookmark");
+
+waitForExplicitFinish();
+add_task(async function testPopup() {
+ info("Checking popup context menu before moving the bookmarks button");
+ await checkPopupContextMenu();
+ let pos = CustomizableUI.getPlacementOfWidget(
+ "bookmarks-menu-button"
+ ).position;
+ let target = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
+ CustomizableUI.addWidgetToArea("bookmarks-menu-button", target);
+ CustomizableUI.addWidgetToArea(
+ "bookmarks-menu-button",
+ CustomizableUI.AREA_NAVBAR,
+ pos
+ );
+ info("Checking popup context menu after moving the bookmarks button");
+ await checkPopupContextMenu();
+ CustomizableUI.reset();
+});
+
+async function checkPopupContextMenu() {
+ let clickTarget = bookmarksMenuButton;
+ BMB_menuPopup.setAttribute("style", "transition: none;");
+ let popupShownPromise = onPopupEvent(BMB_menuPopup, "shown");
+ EventUtils.synthesizeMouseAtCenter(clickTarget, {});
+ info("Waiting for bookmarks menu to be shown.");
+ await popupShownPromise;
+ let contextMenuShownPromise = onPopupEvent(contextMenu, "shown");
+ EventUtils.synthesizeMouseAtCenter(BMB_showAllBookmarks, {
+ type: "contextmenu",
+ button: 2,
+ });
+ info("Waiting for context menu on bookmarks menu to be shown.");
+ await contextMenuShownPromise;
+ ok(
+ !newBookmarkItem.hasAttribute("disabled"),
+ "New bookmark item shouldn't be disabled"
+ );
+ let contextMenuHiddenPromise = onPopupEvent(contextMenu, "hidden");
+ contextMenu.hidePopup();
+ BMB_menuPopup.removeAttribute("style");
+ info("Waiting for context menu on bookmarks menu to be hidden.");
+ await contextMenuHiddenPromise;
+ let popupHiddenPromise = onPopupEvent(BMB_menuPopup, "hidden");
+ // Can't use synthesizeMouseAtCenter because the dropdown panel is in the way
+ EventUtils.synthesizeKey("KEY_Escape");
+ info("Waiting for bookmarks menu to be hidden.");
+ await popupHiddenPromise;
+}
+
+function onPopupEvent(popup, evt) {
+ let fullEvent = "popup" + evt;
+ return new Promise(resolve => {
+ let onPopupHandler = e => {
+ if (e.target == popup) {
+ popup.removeEventListener(fullEvent, onPopupHandler);
+ resolve();
+ }
+ };
+ popup.addEventListener(fullEvent, onPopupHandler);
+ });
+}
diff --git a/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js b/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js
new file mode 100644
index 0000000000..02720cfa2e
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js
@@ -0,0 +1,92 @@
+/**
+ * This test checks that the Show in Folder context menu item in the
+ * bookmarks menu under the app menu actually shows the bookmark in
+ * its folder location in the sidebar.
+ */
+"use strict";
+
+const TEST_PARENT_FOLDER = "The Parent Folder";
+const TEST_URL = "https://example.com/";
+const TEST_TITLE = "Test Bookmark";
+
+let appMenuButton = document.getElementById("PanelUI-menu-button");
+let bookmarksAppMenu = document.getElementById("PanelUI-bookmarks");
+let sidebarWasAlreadyOpen = SidebarUI.isOpen;
+
+const { CustomizableUITestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/CustomizableUITestUtils.sys.mjs"
+);
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+add_task(async function toolbarBookmarkShowInFolder() {
+ let parentFolder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: TEST_PARENT_FOLDER,
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: parentFolder.guid,
+ url: TEST_URL,
+ title: TEST_TITLE,
+ });
+
+ // Open app menu and select bookmarks view
+ await gCUITestUtils.openMainMenu();
+ let appMenuBookmarks = document.getElementById("appMenu-bookmarks-button");
+ appMenuBookmarks.click();
+ let bookmarksView = document.getElementById("PanelUI-bookmarks");
+ let bmViewPromise = BrowserTestUtils.waitForEvent(bookmarksView, "ViewShown");
+ await bmViewPromise;
+
+ // Find the test bookmark and open the context menu on it
+ let list = document.getElementById("panelMenu_bookmarksMenu");
+ let listItem = [...list.children].find(node => node.label == TEST_TITLE);
+ let placesContext = document.getElementById("placesContext");
+ let contextPromise = BrowserTestUtils.waitForEvent(
+ placesContext,
+ "popupshown"
+ );
+ EventUtils.synthesizeMouseAtCenter(listItem, {
+ button: 2,
+ type: "contextmenu",
+ });
+ await contextPromise;
+
+ // Select Show in Folder and wait for the sidebar to show up
+ let sidebarShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "SidebarShown"
+ );
+ placesContext.activateItem(
+ document.getElementById("placesContext_showInFolder")
+ );
+ await sidebarShownPromise;
+
+ // Get the sidebar tree element and find the selected node
+ let sidebar = document.getElementById("sidebar");
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ let treeNode = tree.selectedNode;
+
+ Assert.equal(
+ treeNode.parent.bookmarkGuid,
+ parentFolder.guid,
+ "Containing folder node is correct"
+ );
+ Assert.equal(
+ treeNode.title,
+ listItem.label,
+ "The bookmark title matches selected node"
+ );
+ Assert.equal(
+ treeNode.uri,
+ TEST_URL,
+ "The bookmark URL matches selected node"
+ );
+
+ // Cleanup
+ await PlacesUtils.bookmarks.eraseEverything();
+ if (!sidebarWasAlreadyOpen) {
+ SidebarUI.hide();
+ }
+ await gCUITestUtils.hideMainMenu();
+});
diff --git a/browser/components/places/tests/browser/browser_views_iconsupdate.js b/browser/components/places/tests/browser/browser_views_iconsupdate.js
new file mode 100644
index 0000000000..1799a9665b
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_views_iconsupdate.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+/**
+ * Tests Places views (toolbar, tree) for icons update.
+ * The menu is not tested since it uses the same code as the toolbar.
+ */
+
+add_task(async function () {
+ const PAGE_URI = NetUtil.newURI("http://places.test/");
+ const ICON_URI = NetUtil.newURI(
+ "http://mochi.test:8888/browser/browser/components/places/tests/browser/favicon-normal16.png"
+ );
+
+ info("Uncollapse the personal toolbar if needed");
+ let toolbar = document.getElementById("PersonalToolbar");
+ let wasCollapsed = toolbar.collapsed;
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ registerCleanupFunction(async function () {
+ await promiseSetToolbarVisibility(toolbar, false);
+ });
+ }
+
+ info("Open the bookmarks sidebar");
+ let sidebar = document.getElementById("sidebar");
+ let promiseSidebarLoaded = new Promise(resolve => {
+ sidebar.addEventListener("load", resolve, { capture: true, once: true });
+ });
+ SidebarUI.show("viewBookmarksSidebar");
+ registerCleanupFunction(() => {
+ SidebarUI.hide();
+ });
+ await promiseSidebarLoaded;
+
+ // Add a bookmark to the bookmarks toolbar.
+ let bm = await PlacesUtils.bookmarks.insert({
+ url: PAGE_URI,
+ title: "test icon",
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ });
+ registerCleanupFunction(async function () {
+ await PlacesUtils.bookmarks.remove(bm);
+ });
+
+ // The icon is read asynchronously from the network, we don't have an easy way
+ // to wait for that.
+ await new Promise(resolve => {
+ setTimeout(resolve, 3000);
+ });
+
+ let toolbarElt = getNodeForToolbarItem(bm.guid);
+ let toolbarShot1 = TestUtils.screenshotArea(toolbarElt, window);
+ let sidebarRect = await getRectForSidebarItem(bm.guid);
+ let sidebarShot1 = TestUtils.screenshotArea(sidebarRect, window);
+
+ info("Toolbar: " + toolbarShot1);
+ info("Sidebar: " + sidebarShot1);
+
+ let iconURI = await new Promise(resolve => {
+ PlacesUtils.favicons.setAndFetchFaviconForPage(
+ PAGE_URI,
+ ICON_URI,
+ true,
+ PlacesUtils.favicons.FAVICON_LOAD_NON_PRIVATE,
+ resolve,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ });
+ Assert.ok(iconURI.equals(ICON_URI), "Succesfully set the icon");
+
+ // The icon is read asynchronously from the network, we don't have an easy way
+ // to wait for that, thus we must poll.
+ await TestUtils.waitForCondition(() => {
+ // Assert.notEqual truncates the strings, so it is unusable here for failure
+ // debugging purposes.
+ let toolbarShot2 = TestUtils.screenshotArea(toolbarElt, window);
+ if (toolbarShot1 != toolbarShot2) {
+ info("After toolbar: " + toolbarShot2);
+ }
+ return toolbarShot1 != toolbarShot2;
+ }, "Waiting for the toolbar icon to update");
+
+ await TestUtils.waitForCondition(() => {
+ let sidebarShot2 = TestUtils.screenshotArea(sidebarRect, window);
+ if (sidebarShot1 != sidebarShot2) {
+ info("After sidebar: " + sidebarShot2);
+ }
+ return sidebarShot1 != sidebarShot2;
+ }, "Waiting for the sidebar icon to update");
+});
+
+/**
+ * Get Element for a bookmark in the bookmarks toolbar.
+ *
+ * @param {string} guid
+ * GUID of the item to search.
+ * @returns {object} DOM Node of the element.
+ */
+function getNodeForToolbarItem(guid) {
+ return Array.from(
+ document.getElementById("PlacesToolbarItems").children
+ ).find(child => child._placesNode && child._placesNode.bookmarkGuid == guid);
+}
+
+/**
+ * Get a rect for a bookmark in the bookmarks sidebar
+ *
+ * @param {string} guid
+ * GUID of the item to search.
+ * @returns {object} DOM Node of the element.
+ */
+async function getRectForSidebarItem(guid) {
+ let sidebar = document.getElementById("sidebar");
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ tree.selectItems([guid]);
+ let treerect = tree.getBoundingClientRect();
+ let cellrect = tree.getCoordsForCellItem(
+ tree.currentIndex,
+ tree.columns[0],
+ "cell"
+ );
+
+ // Adjust the position for the tree and sidebar.
+ return {
+ left: treerect.left + cellrect.left + sidebar.getBoundingClientRect().left,
+ top: treerect.top + cellrect.top + sidebar.getBoundingClientRect().top,
+ width: cellrect.width,
+ height: cellrect.height,
+ };
+}
diff --git a/browser/components/places/tests/browser/browser_views_liveupdate.js b/browser/components/places/tests/browser/browser_views_liveupdate.js
new file mode 100644
index 0000000000..cce35941f3
--- /dev/null
+++ b/browser/components/places/tests/browser/browser_views_liveupdate.js
@@ -0,0 +1,493 @@
+/* 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 Places views (menu, toolbar, tree) for liveupdate.
+ */
+
+var toolbar = document.getElementById("PersonalToolbar");
+var wasCollapsed = toolbar.collapsed;
+
+/**
+ * Simulates popup opening causing it to populate.
+ * We cannot just use menu.open, since it would not work on Mac due to native menubar.
+ *
+ * @param {object} aPopup
+ * The popup element
+ */
+function fakeOpenPopup(aPopup) {
+ var popupEvent = document.createEvent("MouseEvent");
+ popupEvent.initMouseEvent(
+ "popupshowing",
+ true,
+ true,
+ window,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ false,
+ false,
+ false,
+ false,
+ 0,
+ null
+ );
+ aPopup.dispatchEvent(popupEvent);
+}
+
+async function testInFolder(folderGuid, prefix) {
+ let addedBookmarks = [];
+ let item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ title: `${prefix}1`,
+ url: `http://${prefix}1.mozilla.org/`,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ item.title = `${prefix}1_edited`;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ title: `${prefix}2`,
+ url: "place:",
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ item.title = `${prefix}2_edited`;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ type: PlacesUtils.bookmarks.TYPE_SEPARATOR,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: folderGuid,
+ title: `${prefix}f`,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ item.title = `${prefix}f_edited`;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item = await PlacesUtils.bookmarks.insert({
+ parentGuid: item.guid,
+ title: `${prefix}f1`,
+ url: `http://${prefix}f1.mozilla.org/`,
+ });
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ addedBookmarks.push(item);
+
+ item.index = 0;
+ item.parentGuid = folderGuid;
+ await PlacesUtils.bookmarks.update(item);
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+
+ return addedBookmarks;
+}
+
+add_task(async function test() {
+ // Uncollapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, true);
+ }
+
+ // Open bookmarks menu.
+ var popup = document.getElementById("bookmarksMenuPopup");
+ ok(popup, "Menu popup element exists");
+ fakeOpenPopup(popup);
+
+ // Open bookmarks sidebar.
+ await withSidebarTree("bookmarks", async () => {
+ // Add observers.
+ bookmarksObserver.handlePlacesEvents =
+ bookmarksObserver.handlePlacesEvents.bind(bookmarksObserver);
+ PlacesUtils.observers.addListener(
+ ["bookmark-added", "bookmark-removed"],
+ bookmarksObserver.handlePlacesEvents
+ );
+ var addedBookmarks = [];
+
+ // MENU
+ info("*** Acting on menu bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.menuGuid, "bm")
+ );
+
+ // TOOLBAR
+ info("*** Acting on toolbar bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.toolbarGuid, "tb")
+ );
+
+ // UNSORTED
+ info("*** Acting on unsorted bookmarks");
+ addedBookmarks = addedBookmarks.concat(
+ await testInFolder(PlacesUtils.bookmarks.unfiledGuid, "ub")
+ );
+
+ // Remove all added bookmarks.
+ for (let bm of addedBookmarks) {
+ // If we remove an item after its containing folder has been removed,
+ // this will throw, but we can ignore that.
+ try {
+ await PlacesUtils.bookmarks.remove(bm);
+ } catch (ex) {}
+ await bookmarksObserver.assertViewsUpdatedCorrectly();
+ }
+
+ // Remove observers.
+ PlacesUtils.observers.removeListener(
+ ["bookmark-added", "bookmark-removed"],
+ bookmarksObserver.handlePlacesEvents
+ );
+ });
+
+ // Collapse the personal toolbar if needed.
+ if (wasCollapsed) {
+ await promiseSetToolbarVisibility(toolbar, false);
+ }
+});
+
+/**
+ * The observer is where magic happens, for every change we do it will look for
+ * nodes positions in the affected views.
+ */
+var bookmarksObserver = {
+ _notifications: [],
+
+ handlePlacesEvents(events) {
+ for (let { type, parentGuid, guid, index } of events) {
+ switch (type) {
+ case "bookmark-added":
+ this._notifications.push([
+ "assertItemAdded",
+ parentGuid,
+ guid,
+ index,
+ ]);
+ break;
+ case "bookmark-removed":
+ this._notifications.push(["assertItemRemoved", parentGuid, guid]);
+ break;
+ }
+ }
+ },
+
+ async assertViewsUpdatedCorrectly() {
+ for (let notification of this._notifications) {
+ let assertFunction = notification.shift();
+
+ let views = await getViewsForFolder(notification.shift());
+ Assert.greater(
+ views.length,
+ 0,
+ "Should have found one or more views for the parent folder."
+ );
+
+ await this[assertFunction](views, ...notification);
+ }
+
+ this._notifications = [];
+ },
+
+ async assertItemAdded(views, guid, expectedIndex) {
+ for (let i = 0; i < views.length; i++) {
+ let [node, index] = searchItemInView(guid, views[i]);
+ Assert.notEqual(node, null, "Should have found the view in " + views[i]);
+ Assert.equal(
+ index,
+ expectedIndex,
+ "Should have found the node at the expected index"
+ );
+ }
+ },
+
+ async assertItemRemoved(views, guid) {
+ for (let i = 0; i < views.length; i++) {
+ let [node] = searchItemInView(guid, views[i]);
+ Assert.equal(node, null, "Should not have found the node");
+ }
+ },
+
+ async assertItemMoved(views, guid, newIndex) {
+ // Check that item has been moved in the correct position.
+ for (let i = 0; i < views.length; i++) {
+ let [node, index] = searchItemInView(guid, views[i]);
+ Assert.notEqual(node, null, "Should have found the view in " + views[i]);
+ Assert.equal(
+ index,
+ newIndex,
+ "Should have found the node at the expected index"
+ );
+ }
+ },
+
+ async assertItemChanged(views, guid, newValue) {
+ let validator = function (aElementOrTreeIndex) {
+ if (typeof aElementOrTreeIndex == "number") {
+ let sidebar = document.getElementById("sidebar");
+ let tree = sidebar.contentDocument.getElementById("bookmarks-view");
+ let cellText = tree.view.getCellText(
+ aElementOrTreeIndex,
+ tree.columns.getColumnAt(0)
+ );
+ if (!newValue) {
+ return (
+ cellText ==
+ PlacesUIUtils.getBestTitle(
+ tree.view.nodeForTreeIndex(aElementOrTreeIndex),
+ true
+ )
+ );
+ }
+ return cellText == newValue;
+ }
+ if (!newValue && aElementOrTreeIndex.localName != "toolbarbutton") {
+ return (
+ aElementOrTreeIndex.getAttribute("label") ==
+ PlacesUIUtils.getBestTitle(aElementOrTreeIndex._placesNode)
+ );
+ }
+ return aElementOrTreeIndex.getAttribute("label") == newValue;
+ };
+
+ for (let i = 0; i < views.length; i++) {
+ let [node, , valid] = searchItemInView(guid, views[i], validator);
+ Assert.notEqual(node, null, "Should have found the view in " + views[i]);
+ Assert.equal(
+ node.title,
+ newValue,
+ "Node should have the correct new title"
+ );
+ Assert.ok(valid, "Node element should have the correct label");
+ }
+ },
+};
+
+/**
+ * Search an item guid in a view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {string} view
+ * either "toolbar", "menu" or "sidebar"
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index, valid] or [null, null, false] if not found.
+ */
+function searchItemInView(itemGuid, view, validator) {
+ switch (view) {
+ case "toolbar":
+ return getNodeForToolbarItem(itemGuid, validator);
+ case "menu":
+ return getNodeForMenuItem(itemGuid, validator);
+ case "sidebar":
+ return getNodeForSidebarItem(itemGuid, validator);
+ }
+
+ return [null, null, false];
+}
+
+/**
+ * Get places node and index for an itemGuid in bookmarks toolbar view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index] or [null, null] if not found.
+ */
+function getNodeForToolbarItem(itemGuid, validator) {
+ var placesToolbarItems = document.getElementById("PlacesToolbarItems");
+
+ function findNode(aContainer) {
+ var children = aContainer.children;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.bookmarkGuid == itemGuid) {
+ let valid = validator ? validator(child) : true;
+ return [child._placesNode, i - staticNodes, valid];
+ }
+
+ // Don't search in queries, they could contain our item in a
+ // different position. Search only folders
+ if (PlacesUtils.nodeIsFolder(child._placesNode)) {
+ var popup = child.menupopup;
+ popup.openPopup();
+ var foundNode = findNode(popup);
+ popup.hidePopup();
+ if (foundNode[0] != null) {
+ return foundNode;
+ }
+ }
+ }
+ return [null, null];
+ }
+
+ return findNode(placesToolbarItems);
+}
+
+/**
+ * Get places node and index for an itemGuid in bookmarks menu view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index] or [null, null] if not found.
+ */
+function getNodeForMenuItem(itemGuid, validator) {
+ var menu = document.getElementById("bookmarksMenu");
+
+ function findNode(aContainer) {
+ var children = aContainer.children;
+ for (var i = 0, staticNodes = 0; i < children.length; i++) {
+ var child = children[i];
+
+ // Is this a Places node?
+ if (!child._placesNode) {
+ staticNodes++;
+ continue;
+ }
+
+ if (child._placesNode.bookmarkGuid == itemGuid) {
+ let valid = validator ? validator(child) : true;
+ return [child._placesNode, i - staticNodes, valid];
+ }
+
+ // Don't search in queries, they could contain our item in a
+ // different position. Search only folders
+ if (PlacesUtils.nodeIsFolder(child._placesNode)) {
+ var popup = child.lastElementChild;
+ fakeOpenPopup(popup);
+ var foundNode = findNode(popup);
+
+ child.open = false;
+ if (foundNode[0] != null) {
+ return foundNode;
+ }
+ }
+ }
+ return [null, null, false];
+ }
+
+ return findNode(menu.lastElementChild);
+}
+
+/**
+ * Get places node and index for an itemGuid in sidebar tree view.
+ *
+ * @param {string} itemGuid
+ * item guid of the item to search.
+ * @param {Function} validator
+ * function to check validity of the found node element.
+ * @returns {Array}
+ * [node, index] or [null, null] if not found.
+ */
+function getNodeForSidebarItem(itemGuid, validator) {
+ var sidebar = document.getElementById("sidebar");
+ var tree = sidebar.contentDocument.getElementById("bookmarks-view");
+
+ function findNode(aContainerIndex) {
+ if (tree.view.isContainerEmpty(aContainerIndex)) {
+ return [null, null, false];
+ }
+
+ // The rowCount limit is just for sanity, but we will end looping when
+ // we have checked the last child of this container or we have found node.
+ for (var i = aContainerIndex + 1; i < tree.view.rowCount; i++) {
+ var node = tree.view.nodeForTreeIndex(i);
+
+ if (node.bookmarkGuid == itemGuid) {
+ // Minus one because we want relative index inside the container.
+ let valid = validator ? validator(i) : true;
+ return [node, i - tree.view.getParentIndex(i) - 1, valid];
+ }
+
+ if (PlacesUtils.nodeIsFolder(node)) {
+ // Open container.
+ tree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ tree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null) {
+ return foundNode;
+ }
+ }
+
+ // We have finished walking this container.
+ if (!tree.view.hasNextSibling(aContainerIndex + 1, i)) {
+ break;
+ }
+ }
+ return [null, null, false];
+ }
+
+ // Root node is hidden, so we need to manually walk the first level.
+ for (var i = 0; i < tree.view.rowCount; i++) {
+ // Open container.
+ tree.view.toggleOpenState(i);
+ // Search inside it.
+ var foundNode = findNode(i);
+ // Close container.
+ tree.view.toggleOpenState(i);
+ // Return node if found.
+ if (foundNode[0] != null) {
+ return foundNode;
+ }
+ }
+ return [null, null, false];
+}
+
+/**
+ * Get views affected by changes to a folder.
+ *
+ * @param {string} folderGuid
+ * item guid of the folder we have changed.
+ * @returns {Array<"toolbar" | "menu" | "sidebar">}
+ * subset of views: ["toolbar", "menu", "sidebar"]
+ */
+async function getViewsForFolder(folderGuid) {
+ let rootGuid = folderGuid;
+ while (!PlacesUtils.isRootItem(rootGuid)) {
+ let itemData = await PlacesUtils.bookmarks.fetch(rootGuid);
+ rootGuid = itemData.parentGuid;
+ }
+
+ switch (rootGuid) {
+ case PlacesUtils.bookmarks.toolbarGuid:
+ return ["toolbar", "sidebar"];
+ case PlacesUtils.bookmarks.menuGuid:
+ return ["menu", "sidebar"];
+ case PlacesUtils.bookmarks.unfiledGuid:
+ return ["sidebar"];
+ }
+ return [];
+}
diff --git a/browser/components/places/tests/browser/favicon-normal16.png b/browser/components/places/tests/browser/favicon-normal16.png
new file mode 100644
index 0000000000..62b69a3d03
--- /dev/null
+++ b/browser/components/places/tests/browser/favicon-normal16.png
Binary files differ
diff --git a/browser/components/places/tests/browser/frameLeft.html b/browser/components/places/tests/browser/frameLeft.html
new file mode 100644
index 0000000000..5a54fe353b
--- /dev/null
+++ b/browser/components/places/tests/browser/frameLeft.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Left frame</title>
+ </head>
+ <body>
+ <a id="clickme" href="frameRight.html" target="right">Open page in the right frame.</a>
+ </body>
+</html>
diff --git a/browser/components/places/tests/browser/frameRight.html b/browser/components/places/tests/browser/frameRight.html
new file mode 100644
index 0000000000..226accc349
--- /dev/null
+++ b/browser/components/places/tests/browser/frameRight.html
@@ -0,0 +1,8 @@
+<html>
+ <head>
+ <title>Right Frame</title>
+ </head>
+ <body>
+ This is the right frame.
+ </body>
+</html>
diff --git a/browser/components/places/tests/browser/framedPage.html b/browser/components/places/tests/browser/framedPage.html
new file mode 100644
index 0000000000..58c5bbd79e
--- /dev/null
+++ b/browser/components/places/tests/browser/framedPage.html
@@ -0,0 +1,9 @@
+<html>
+ <head>
+ <title>Framed page</title>
+ </head>
+ <frameset cols="*,*">
+ <frame id="left" name="left" src="frameLeft.html">
+ <frame id="right" name="right" src="about:mozilla">
+ </frameset>
+</html>
diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js
new file mode 100644
index 0000000000..21790d54aa
--- /dev/null
+++ b/browser/components/places/tests/browser/head.js
@@ -0,0 +1,570 @@
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(this, "gFluentStrings", function () {
+ return new Localization(["branding/brand.ftl", "browser/browser.ftl"], true);
+});
+
+function openLibrary(callback, aLeftPaneRoot) {
+ let library = window.openDialog(
+ "chrome://browser/content/places/places.xhtml",
+ "",
+ "chrome,toolbar=yes,dialog=no,resizable",
+ aLeftPaneRoot
+ );
+ waitForFocus(function () {
+ checkLibraryPaneVisibility(library, aLeftPaneRoot);
+ callback(library);
+ }, library);
+
+ return library;
+}
+
+/**
+ * Returns a handle to a Library window.
+ * If one is opens returns itm otherwise it opens a new one.
+ *
+ * @param {object} aLeftPaneRoot
+ * Hierarchy to open and select in the left pane.
+ * @returns {Promise}
+ * Resolves to the handle to the library window.
+ */
+function promiseLibrary(aLeftPaneRoot) {
+ return new Promise(resolve => {
+ let library = Services.wm.getMostRecentWindow("Places:Organizer");
+ if (library && !library.closed) {
+ if (aLeftPaneRoot) {
+ library.PlacesOrganizer.selectLeftPaneContainerByHierarchy(
+ aLeftPaneRoot
+ );
+ }
+ checkLibraryPaneVisibility(library, aLeftPaneRoot);
+ resolve(library);
+ } else {
+ openLibrary(resolve, aLeftPaneRoot);
+ }
+ });
+}
+
+function promiseLibraryClosed(organizer) {
+ return new Promise(resolve => {
+ if (organizer.closed) {
+ resolve();
+ return;
+ }
+ // Wait for the Organizer window to actually be closed
+ organizer.addEventListener(
+ "unload",
+ function () {
+ executeSoon(resolve);
+ },
+ { once: true }
+ );
+
+ // Close Library window.
+ organizer.close();
+ });
+}
+
+function checkLibraryPaneVisibility(library, selectedPane) {
+ // Make sure right view is shown
+ if (selectedPane == "Downloads") {
+ Assert.ok(
+ library.ContentTree.view.hidden,
+ "Bookmark/History tree is hidden"
+ );
+ Assert.ok(
+ !library.document.getElementById("downloadsListBox").hidden,
+ "Downloads are shown"
+ );
+ } else {
+ Assert.ok(
+ !library.ContentTree.view.hidden,
+ "Bookmark/History tree is shown"
+ );
+ Assert.ok(
+ library.document.getElementById("downloadsListBox").hidden,
+ "Downloads are hidden"
+ );
+ }
+
+ // Check currentView getter
+ Assert.ok(!library.ContentArea.currentView.hidden, "Current view is shown");
+}
+
+/**
+ * Waits for a clipboard operation to complete, looking for the expected type.
+ *
+ * @see waitForClipboard
+ *
+ * @param {Function} aPopulateClipboardFn
+ * Function to populate the clipboard.
+ * @param {string} aFlavor
+ * Data flavor to expect.
+ * @returns {Promise}
+ * A promise that is resolved with the data.
+ */
+function promiseClipboard(aPopulateClipboardFn, aFlavor) {
+ return new Promise((resolve, reject) => {
+ waitForClipboard(
+ data => !!data,
+ aPopulateClipboardFn,
+ resolve,
+ reject,
+ aFlavor
+ );
+ });
+}
+
+function synthesizeClickOnSelectedTreeCell(aTree, aOptions) {
+ if (aTree.view.selection.count < 1) {
+ throw new Error("The test node should be successfully selected");
+ }
+ // Get selection rowID.
+ let min = {},
+ max = {};
+ aTree.view.selection.getRangeAt(0, min, max);
+ let rowID = min.value;
+ aTree.ensureRowIsVisible(rowID);
+ // Calculate the click coordinates.
+ var rect = aTree.getCoordsForCellItem(rowID, aTree.columns[0], "text");
+ var x = rect.x + rect.width / 2;
+ var y = rect.y + rect.height / 2;
+ if (aTree.id == "bookmarks-view" || aTree.id == "historyTree") {
+ // We are purposefully keeping the main <tree> element unlabeled, because in
+ // this specific case, the on-screen label for either "Bookmarks" or
+ // "History" sidebar is positioned closely to the tree, visually and in DOM.
+ // We want to avoid making a screen reader user to listen to a redundant
+ // announcement, therefore no accessible name is provided to the container
+ // and we account for this in a11y-checks:
+ AccessibilityUtils.setEnv({
+ labelRule: false,
+ });
+ }
+ // Simulate the click.
+ EventUtils.synthesizeMouse(
+ aTree.body,
+ x,
+ y,
+ aOptions || {},
+ aTree.ownerGlobal
+ );
+ AccessibilityUtils.resetEnv();
+}
+
+/**
+ * Makes the specified toolbar visible or invisible and returns a Promise object
+ * that is resolved when the toolbar has completed any animations associated
+ * with hiding or showing the toolbar.
+ *
+ * Note that this code assumes that changes to a toolbar's visibility trigger
+ * a transition on the max-height property of the toolbar element.
+ * Changes to this styling could cause the returned Promise object to be
+ * resolved too early or not at all.
+ *
+ * @param {object} aToolbar
+ * The toolbar to update.
+ * @param {boolean} aVisible
+ * True to make the toolbar visible, false to make it hidden.
+ *
+ * @returns {Promise} Any animation associated with updating the toolbar's
+ * visibility has finished.
+ */
+function promiseSetToolbarVisibility(aToolbar, aVisible) {
+ if (isToolbarVisible(aToolbar) != aVisible) {
+ let visibilityChanged = TestUtils.waitForCondition(
+ () => aToolbar.collapsed != aVisible
+ );
+ setToolbarVisibility(aToolbar, aVisible, undefined, false);
+ return visibilityChanged;
+ }
+ return Promise.resolve();
+}
+
+/**
+ * Helper function to determine if the given toolbar is in the visible
+ * state according to its autohide/collapsed attribute.
+ *
+ * @param {object} aToolbar The toolbar to query.
+ *
+ * @returns {boolean} True if the relevant attribute on |aToolbar| indicates it is
+ * visible, false otherwise.
+ */
+function isToolbarVisible(aToolbar) {
+ let hidingAttribute =
+ aToolbar.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
+ let hidingValue = aToolbar.getAttribute(hidingAttribute).toLowerCase();
+ // Check for both collapsed="true" and collapsed="collapsed"
+ return hidingValue !== "true" && hidingValue !== hidingAttribute;
+}
+
+/**
+ * Executes a task after opening the bookmarks dialog, then cancels the dialog.
+ *
+ * @param {boolean} autoCancel
+ * whether to automatically cancel the dialog at the end of the task
+ * @param {Function} openFn
+ * generator function causing the dialog to open
+ * @param {Function} taskFn
+ * the task to execute once the dialog is open
+ * @param {Function} closeFn
+ * A function to be used to wait for pending work when the dialog is
+ * closing. It is passed the dialog window handle and should return a promise.
+ * @returns {string} guid
+ * Bookmark guid
+ */
+var withBookmarksDialog = async function (autoCancel, openFn, taskFn, closeFn) {
+ let dialogUrl = "chrome://browser/content/places/bookmarkProperties.xhtml";
+ let closed = false;
+ // We can't show the in-window prompt for windows which don't have
+ // gDialogBox, like the library (Places:Organizer) window.
+ let hasDialogBox = !!Services.wm.getMostRecentWindow("").gDialogBox;
+ let dialogPromise;
+ if (hasDialogBox) {
+ dialogPromise = BrowserTestUtils.promiseAlertDialogOpen(null, dialogUrl, {
+ isSubDialog: true,
+ });
+ } else {
+ dialogPromise = BrowserTestUtils.domWindowOpenedAndLoaded(null, win => {
+ return win.document.documentURI.startsWith(dialogUrl);
+ }).then(win => {
+ ok(
+ win.location.href.startsWith(dialogUrl),
+ "The bookmark properties dialog is open: " + win.location.href
+ );
+ // This is needed for the overlay.
+ return SimpleTest.promiseFocus(win).then(() => win);
+ });
+ }
+ let dialogClosePromise = dialogPromise.then(win => {
+ if (!hasDialogBox) {
+ return BrowserTestUtils.domWindowClosed(win);
+ }
+ let container = win.top.document.getElementById("window-modal-dialog");
+ return BrowserTestUtils.waitForEvent(container, "close").then(() => {
+ return BrowserTestUtils.waitForMutationCondition(
+ container,
+ { childList: true, attributes: true },
+ () => !container.hasChildNodes() && !container.open
+ );
+ });
+ });
+ dialogClosePromise.then(() => {
+ closed = true;
+ });
+
+ info("withBookmarksDialog: opening the dialog");
+ // The dialog might be modal and could block our events loop, so executeSoon.
+ executeSoon(openFn);
+
+ info("withBookmarksDialog: waiting for the dialog");
+ let dialogWin = await dialogPromise;
+
+ // Ensure overlay is loaded
+ info("waiting for the overlay to be loaded");
+ await dialogWin.document.mozSubdialogReady;
+
+ // Check the first input is focused.
+ let doc = dialogWin.document;
+ let elt = doc.querySelector('input:not([hidden="true"])');
+ ok(elt, "There should be an input to focus.");
+
+ if (elt) {
+ info("waiting for focus on the first textfield");
+ await TestUtils.waitForCondition(
+ () => doc.activeElement == elt,
+ "The first non collapsed input should have been focused"
+ );
+ }
+
+ info("withBookmarksDialog: executing the task");
+
+ let closePromise = () => Promise.resolve();
+ if (closeFn) {
+ closePromise = closeFn(dialogWin);
+ }
+ let guid;
+ try {
+ await taskFn(dialogWin);
+ } finally {
+ if (!closed && autoCancel) {
+ info("withBookmarksDialog: canceling the dialog");
+ doc.getElementById("bookmarkpropertiesdialog").cancelDialog();
+ await closePromise;
+ }
+ guid = await PlacesUIUtils.lastBookmarkDialogDeferred.promise;
+ // Give the dialog a little time to close itself.
+ await dialogClosePromise;
+ }
+ return guid;
+};
+
+/**
+ * Opens the contextual menu on the element pointed by the given selector.
+ *
+ * @param {object} browser
+ * The associated browser element.
+ * @param {object} selector
+ * Valid selector syntax
+ * @returns {Promise}
+ * Returns a Promise that resolves once the context menu has been
+ * opened.
+ */
+var openContextMenuForContentSelector = async function (browser, selector) {
+ info("wait for the context menu");
+ let contextPromise = BrowserTestUtils.waitForEvent(
+ document.getElementById("contentAreaContextMenu"),
+ "popupshown"
+ );
+ await SpecialPowers.spawn(browser, [{ selector }], async function (args) {
+ let doc = content.document;
+ let elt = doc.querySelector(args.selector);
+ dump(`openContextMenuForContentSelector: found ${elt}\n`);
+
+ /* Open context menu so chrome can access the element */
+ const domWindowUtils = content.windowUtils;
+ let rect = elt.getBoundingClientRect();
+ let left = rect.left + rect.width / 2;
+ let top = rect.top + rect.height / 2;
+ domWindowUtils.sendMouseEvent(
+ "contextmenu",
+ left,
+ top,
+ 2,
+ 1,
+ 0,
+ false,
+ 0,
+ 0,
+ true
+ );
+ });
+ await contextPromise;
+};
+
+/**
+ * Fills a bookmarks dialog text field ensuring to cause expected edit events.
+ *
+ * @param {string} id
+ * id of the text field
+ * @param {string} text
+ * text to fill in
+ * @param {object} win
+ * dialog window
+ * @param {boolean} [blur]
+ * whether to blur at the end.
+ */
+function fillBookmarkTextField(id, text, win, blur = true) {
+ let elt = win.document.getElementById(id);
+ elt.focus();
+ elt.select();
+ if (!text) {
+ EventUtils.synthesizeKey("VK_DELETE", {}, win);
+ } else {
+ for (let c of text.split("")) {
+ EventUtils.synthesizeKey(c, {}, win);
+ }
+ }
+ if (blur) {
+ elt.blur();
+ }
+}
+
+/**
+ * Executes a task after opening the bookmarks or history sidebar. Takes care
+ * of closing the sidebar once done.
+ *
+ * @param {string} type
+ * either "bookmarks" or "history".
+ * @param {Function} taskFn
+ * The task to execute once the sidebar is ready. Will get the Places
+ * tree view as input.
+ */
+var withSidebarTree = async function (type, taskFn) {
+ let sidebar = document.getElementById("sidebar");
+ info("withSidebarTree: waiting sidebar load");
+ let sidebarLoadedPromise = new Promise(resolve => {
+ sidebar.addEventListener(
+ "load",
+ function () {
+ executeSoon(resolve);
+ },
+ { capture: true, once: true }
+ );
+ });
+ let sidebarId =
+ type == "bookmarks" ? "viewBookmarksSidebar" : "viewHistorySidebar";
+ SidebarUI.show(sidebarId);
+ await sidebarLoadedPromise;
+
+ let treeId = type == "bookmarks" ? "bookmarks-view" : "historyTree";
+ let tree = sidebar.contentDocument.getElementById(treeId);
+
+ // Need to executeSoon since the tree is initialized on sidebar load.
+ info("withSidebarTree: executing the task");
+ try {
+ await taskFn(tree);
+ } finally {
+ SidebarUI.hide();
+ }
+};
+
+/**
+ * Executes a task after opening the Library on a given root. Takes care
+ * of closing the library once done.
+ *
+ * @param {string} hierarchy
+ * The left pane hierarchy to open.
+ * @param {Function} taskFn
+ * The task to execute once the Library is ready.
+ * Will get { left, right } trees as argument.
+ */
+var withLibraryWindow = async function (hierarchy, taskFn) {
+ let library = await promiseLibrary(hierarchy);
+ let left = library.document.getElementById("placesList");
+ let right = library.document.getElementById("placeContent");
+ info("withLibrary: executing the task");
+ try {
+ await taskFn({ left, right });
+ } finally {
+ await promiseLibraryClosed(library);
+ }
+};
+
+function promisePlacesInitComplete() {
+ const gBrowserGlue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+ );
+
+ let placesInitCompleteObserved = TestUtils.topicObserved(
+ "places-browser-init-complete"
+ );
+
+ gBrowserGlue.observe(
+ null,
+ "browser-glue-test",
+ "places-browser-init-complete"
+ );
+
+ return placesInitCompleteObserved;
+}
+
+// Function copied from browser/base/content/test/general/head.js.
+function promisePopupShown(popup) {
+ return new Promise(resolve => {
+ if (popup.state == "open") {
+ resolve();
+ } else {
+ let onPopupShown = event => {
+ popup.removeEventListener("popupshown", onPopupShown);
+ resolve();
+ };
+ popup.addEventListener("popupshown", onPopupShown);
+ }
+ });
+}
+
+// Function copied from browser/base/content/test/general/head.js.
+function promisePopupHidden(popup) {
+ return new Promise(resolve => {
+ let onPopupHidden = event => {
+ popup.removeEventListener("popuphidden", onPopupHidden);
+ resolve();
+ };
+ popup.addEventListener("popuphidden", onPopupHidden);
+ });
+}
+
+// Identify a bookmark node in the Bookmarks Toolbar by its guid.
+function getToolbarNodeForItemGuid(itemGuid) {
+ let children = document.getElementById("PlacesToolbarItems").childNodes;
+ for (let child of children) {
+ if (itemGuid === child._placesNode.bookmarkGuid) {
+ return child;
+ }
+ }
+ return null;
+}
+
+// Open the bookmarks Star UI by clicking the star button on the address bar.
+async function clickBookmarkStar(win = window) {
+ let shownPromise = promisePopupShown(
+ win.document.getElementById("editBookmarkPanel")
+ );
+ win.BookmarkingUI.star.click();
+ await shownPromise;
+
+ // Additionally await for the async init to complete.
+ let menuList = win.document.getElementById("editBMPanel_folderMenuList");
+ await BrowserTestUtils.waitForMutationCondition(
+ menuList,
+ { attributes: true },
+ () => !!menuList.getAttribute("selectedGuid"),
+ "Should select the menu folder item"
+ );
+}
+
+// Close the bookmarks Star UI by clicking the "Done" button.
+async function hideBookmarksPanel(win = window) {
+ let hiddenPromise = promisePopupHidden(
+ win.document.getElementById("editBookmarkPanel")
+ );
+ // Confirm and close the dialog.
+ win.document.getElementById("editBookmarkPanelDoneButton").click();
+ await hiddenPromise;
+}
+
+// Create a temporary folder, set it as the default folder,
+// then remove the folder. This is used to ensure that the
+// default folder gets reset properly.
+async function createAndRemoveDefaultFolder() {
+ let tempFolder = await PlacesUtils.bookmarks.insertTree({
+ guid: PlacesUtils.bookmarks.unfiledGuid,
+ children: [
+ {
+ title: "temp folder",
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ },
+ ],
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.bookmarks.defaultLocation", tempFolder[0].guid]],
+ });
+
+ let defaultGUID = await PlacesUIUtils.defaultParentGuid;
+ is(defaultGUID, tempFolder[0].guid, "check default guid");
+
+ await PlacesUtils.bookmarks.remove(tempFolder);
+}
+
+async function showLibraryColumn(library, columnName) {
+ const viewMenu = library.document.getElementById("viewMenu");
+ const viewMenuPopup = library.document.getElementById("viewMenuPopup");
+ const onViewMenuPopup = new Promise(resolve => {
+ viewMenuPopup.addEventListener("popupshown", () => resolve(), {
+ once: true,
+ });
+ });
+ EventUtils.synthesizeMouseAtCenter(viewMenu, {}, library);
+ await onViewMenuPopup;
+
+ const viewColumns = library.document.getElementById("viewColumns");
+ const viewColumnsPopup = viewColumns.querySelector("menupopup");
+ const onViewColumnsPopup = new Promise(resolve => {
+ viewColumnsPopup.addEventListener("popupshown", () => resolve(), {
+ once: true,
+ });
+ });
+ EventUtils.synthesizeMouseAtCenter(viewColumns, {}, library);
+ await onViewColumnsPopup;
+
+ const columnMenu = library.document.getElementById(`menucol_${columnName}`);
+ EventUtils.synthesizeMouseAtCenter(columnMenu, {}, library);
+}
+
+registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.bookmarks.defaultLocation");
+ await PlacesTransactions.clearTransactionsHistory(true, true);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser.toml b/browser/components/places/tests/browser/interactions/browser.toml
new file mode 100644
index 0000000000..ce39403409
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser.toml
@@ -0,0 +1,36 @@
+[DEFAULT]
+prefs = [
+ "browser.places.interactions.enabled=true",
+ "browser.places.interactions.log=true",
+ "browser.places.interactions.scrolling_timeout_ms=50",
+ "general.smoothScroll=false",
+]
+
+support-files = [
+ "head.js",
+ "../keyword_form.html",
+ "scrolling.html",
+ "scrolling_subframe.html",
+]
+
+["browser_interactions_blocklist.js"]
+
+["browser_interactions_clearHistory.js"]
+
+["browser_interactions_disabledHistory.js"]
+
+["browser_interactions_referrer.js"]
+
+["browser_interactions_scrolling.js"]
+skip-if = ["apple_silicon && fission"] # Disabled due to bleedover with other tests when run in regular suites; passes in "failures" jobs
+
+["browser_interactions_scrolling_dom_history.js"]
+skip-if = ["os == 'mac'"] # Bug 1756157: Randomly times out on macOS
+
+["browser_interactions_typing.js"]
+
+["browser_interactions_typing_dom_history.js"]
+
+["browser_interactions_view_time.js"]
+
+["browser_interactions_view_time_dom_history.js"]
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js b/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js
new file mode 100644
index 0000000000..e7a880637b
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js
@@ -0,0 +1,105 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that interactions are not recorded for sites on the blocklist.
+ */
+
+const ALLOWED_TEST_URL = "http://mochi.test:8888/";
+const BLOCKED_TEST_URL = "https://example.com/browser";
+
+ChromeUtils.defineESModuleGetters(this, {
+ FilterAdult: "resource://activity-stream/lib/FilterAdult.sys.mjs",
+ InteractionsBlocklist: "resource:///modules/InteractionsBlocklist.sys.mjs",
+});
+
+add_setup(async function () {
+ let oldBlocklistValue = Services.prefs.getStringPref(
+ "places.interactions.customBlocklist",
+ "[]"
+ );
+ Services.prefs.setStringPref("places.interactions.customBlocklist", "[]");
+
+ registerCleanupFunction(async () => {
+ Services.prefs.setStringPref(
+ "places.interactions.customBlocklist",
+ oldBlocklistValue
+ );
+ });
+});
+/**
+ * Loads the blocked URL, then loads about:blank to trigger the end of the
+ * interaction with the blocked URL.
+ *
+ * @param {boolean} expectRecording
+ * True if we expect the blocked URL to have been recorded in the database.
+ */
+async function loadBlockedUrl(expectRecording) {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(ALLOWED_TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.startLoadingURIString(browser, BLOCKED_TEST_URL);
+ await BrowserTestUtils.browserLoaded(browser, false, BLOCKED_TEST_URL);
+
+ await assertDatabaseValues([
+ {
+ url: ALLOWED_TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ if (expectRecording) {
+ await assertDatabaseValues([
+ {
+ url: ALLOWED_TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: BLOCKED_TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+ } else {
+ await assertDatabaseValues([
+ {
+ url: ALLOWED_TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ }
+ });
+}
+
+add_task(async function test_regexp() {
+ info("Record BLOCKED_TEST_URL because it is not yet blocklisted.");
+ await loadBlockedUrl(true);
+
+ info("Add BLOCKED_TEST_URL to the blocklist and verify it is not recorded.");
+ let blockedRegex = /^(https?:\/\/)?example\.com\/browser/i;
+ InteractionsBlocklist.addRegexToBlocklist(blockedRegex);
+ Assert.equal(
+ Services.prefs.getStringPref("places.interactions.customBlocklist", "[]"),
+ JSON.stringify([blockedRegex.toString()])
+ );
+ await loadBlockedUrl(false);
+
+ info("Remove BLOCKED_TEST_URL from the blocklist and verify it is recorded.");
+ InteractionsBlocklist.removeRegexFromBlocklist(blockedRegex);
+ Assert.equal(
+ Services.prefs.getStringPref("places.interactions.customBlocklist", "[]"),
+ JSON.stringify([])
+ );
+ await loadBlockedUrl(true);
+});
+
+add_task(async function test_adult() {
+ FilterAdult.addDomainToList("https://example.com/browser");
+ await loadBlockedUrl(false);
+ FilterAdult.removeDomainFromList("https://example.com/browser");
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_clearHistory.js b/browser/components/places/tests/browser/interactions/browser_interactions_clearHistory.js
new file mode 100644
index 0000000000..37b99ba974
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_clearHistory.js
@@ -0,0 +1,106 @@
+/* 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 we clear interactions when history is cleared.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html";
+const TEST_URL_2 =
+ "https://example.org/browser/browser/components/places/tests/browser/keyword_form.html";
+const TEST_URL_AWAY = "https://example.com/browser";
+
+const sentence = "The quick brown fox jumps over the lazy dog.";
+
+async function sendTextToInput(browser, text) {
+ // Reset to later verify that the provided text matches the value.
+ await SpecialPowers.spawn(browser, [], function () {
+ const input = content.document.querySelector(
+ "#form1 > input[name='search']"
+ );
+ input.focus();
+ input.value = "";
+ });
+
+ EventUtils.sendString(text);
+
+ await SpecialPowers.spawn(browser, [{ text }], async function (args) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#form1 > input[name='search']").value ==
+ args.text,
+ "Text has been set on input"
+ );
+ });
+}
+
+add_setup(async function () {
+ await Interactions.reset();
+ await PlacesUtils.bookmarks.insert({
+ url: TEST_URL,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ registerCleanupFunction(async () => {
+ await Interactions.reset();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_clear_history() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL_2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL_2);
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+ });
+
+ Assert.ok(
+ await PlacesUtils.history.hasVisits(TEST_URL),
+ "Check visits were added"
+ );
+ Assert.ok(
+ await PlacesUtils.history.hasVisits(TEST_URL_2),
+ "Check visits were added"
+ );
+
+ info("Check interactions were added");
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL_2,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ await PlacesUtils.history.clear();
+
+ Assert.ok(
+ await PlacesTestUtils.isPageInDB(TEST_URL),
+ "Bookmarked page remains in the database"
+ );
+ Assert.ok(
+ !(await PlacesUtils.history.hasVisits(TEST_URL)),
+ "Check visits were removed"
+ );
+ Assert.ok(
+ !(await PlacesTestUtils.isPageInDB(TEST_URL_2)),
+ "Non bookmarked page was removed from the database"
+ );
+
+ info("Check all interactions have been removed.");
+ await assertDatabaseValues([]);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_disabledHistory.js b/browser/components/places/tests/browser/interactions/browser_interactions_disabledHistory.js
new file mode 100644
index 0000000000..a5a62207cf
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_disabledHistory.js
@@ -0,0 +1,102 @@
+/* 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 we don't record interactions when history is disabled, even if the page
+ * is bookmarked.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html";
+const TEST_URL_AWAY = "https://example.com/browser";
+
+const sentence = "The quick brown fox jumps over the lazy dog.";
+
+async function sendTextToInput(browser, text) {
+ // Reset to later verify that the provided text matches the value.
+ await SpecialPowers.spawn(browser, [], function () {
+ const input = content.document.querySelector(
+ "#form1 > input[name='search']"
+ );
+ input.focus();
+ input.value = "";
+ });
+
+ EventUtils.sendString(text);
+
+ await SpecialPowers.spawn(browser, [{ text }], async function (args) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#form1 > input[name='search']").value ==
+ args.text,
+ "Text has been set on input"
+ );
+ });
+}
+
+add_setup(async function () {
+ await Interactions.reset();
+ await PlacesUtils.bookmarks.insert({
+ url: TEST_URL,
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ });
+ registerCleanupFunction(async () => {
+ await Interactions.reset();
+ await PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+add_task(async function test_disabled_history() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["places.history.enabled", false]],
+ });
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL_AWAY);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL_AWAY);
+
+ await assertDatabaseValues([]);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([]);
+ });
+ await SpecialPowers.popPrefEnv();
+});
+
+add_task(async function test_disableGlobalHistory() {
+ await Interactions.reset();
+
+ await PlacesUtils.history.clear();
+ let tab = (gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser));
+ let browser = tab.linkedBrowser;
+ browser.setAttribute("disableglobalhistory", "true");
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
+ Assert.ok(
+ !browser.browsingContext.useGlobalHistory,
+ "browserContext should be updated after the first load"
+ );
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL_AWAY);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL_AWAY);
+
+ Assert.ok(
+ !(await PlacesUtils.history.hasVisits(TEST_URL)),
+ "Check visits were not added"
+ );
+ Assert.ok(
+ !(await PlacesUtils.history.hasVisits(TEST_URL_AWAY)),
+ "Check visits were not added"
+ );
+ await assertDatabaseValues([]);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js b/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js
new file mode 100644
index 0000000000..fda93bfc5a
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_referrer.js
@@ -0,0 +1,45 @@
+/* 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 page view time recording for interactions.
+ */
+
+const TEST_REFERRER_URL = "https://example.org/browser";
+const TEST_URL = "https://example.org/browser/browser";
+
+add_task(async function test_interactions_referrer() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_REFERRER_URL, async browser => {
+ let ReferrerInfo = Components.Constructor(
+ "@mozilla.org/referrer-info;1",
+ "nsIReferrerInfo",
+ "init"
+ );
+
+ // Load a new URI with a specific referrer.
+ let referrerInfo1 = new ReferrerInfo(
+ Ci.nsIReferrerInfo.EMPTY,
+ true,
+ Services.io.newURI(TEST_REFERRER_URL)
+ );
+ browser.loadURI(Services.io.newURI(TEST_URL), {
+ referrerInfo: referrerInfo1,
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal(
+ {}
+ ),
+ });
+ await BrowserTestUtils.browserLoaded(browser, true, TEST_URL);
+ });
+ await assertDatabaseValues([
+ {
+ url: TEST_REFERRER_URL,
+ referrer_url: null,
+ },
+ {
+ url: TEST_URL,
+ referrer_url: TEST_REFERRER_URL,
+ },
+ ]);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js
new file mode 100644
index 0000000000..59428a485d
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js
@@ -0,0 +1,162 @@
+/* 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 reporting of scrolling interactions.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/interactions/scrolling.html";
+const TEST_URL2 = "https://example.com/browser";
+
+async function waitForScrollEvent(aBrowser, aTask) {
+ let promise = BrowserTestUtils.waitForContentEvent(aBrowser, "scroll");
+
+ // This forces us to send a message to the browser's process and receive a response which ensures
+ // that the message sent to register the scroll event listener will also have been processed by
+ // the content process. Without this it is possible for our scroll task to send a higher priority
+ // message which can be processed by the content process before the message to register the scroll
+ // event listener.
+ await SpecialPowers.spawn(aBrowser, [], () => {});
+
+ await aTask();
+ await promise;
+}
+
+add_task(async function test_no_scrolling() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_arrow_key_down_scroll() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scrollIntoView() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("middleHeading");
+ heading.scrollIntoView();
+ })
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // JS-triggered scrolling should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_anchor_click() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ const anchor = content.document.getElementById("to_bottom_anchor");
+ anchor.click();
+ })
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // The scrolling resulting from clicking on an anchor should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_window_scrollBy() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ content.scrollBy(0, 100);
+ })
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // The scrolling resulting from the window.scrollBy() call should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_window_scrollTo() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await waitForScrollEvent(browser, () =>
+ SpecialPowers.spawn(browser, [], function () {
+ content.scrollTo(0, 200);
+ })
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ // The scrolling resulting from the window.scrollTo() call should not be reported
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactscrollingDistance: 0,
+ exactscrollingTime: 0,
+ },
+ ]);
+ });
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js
new file mode 100644
index 0000000000..b825ce615e
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js
@@ -0,0 +1,208 @@
+/* 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 reporting of scrolling interactions after DOM history API use.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/interactions/scrolling.html";
+const TEST_URL2 = "https://example.com/browser";
+
+async function waitForScrollEvent(aBrowser, aTask) {
+ let promise = BrowserTestUtils.waitForContentEvent(aBrowser, "scroll");
+
+ // This forces us to send a message to the browser's process and receive a response which ensures
+ // that the message sent to register the scroll event listener will also have been processed by
+ // the content process. Without this it is possible for our scroll task to send a higher priority
+ // message which can be processed by the content process before the message to register the scroll
+ // event listener.
+ await SpecialPowers.spawn(aBrowser, [], () => {});
+
+ await aTask();
+ await promise;
+}
+
+add_task(async function test_scroll_pushState() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_pushState_sameUrl() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ // As the page hasn't changed there will be no interactions saved yet.
+ await assertDatabaseValues([]);
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_replaceState() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_replaceState_sameUrl() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ // As the page hasn't changed there will be no interactions saved yet.
+ await assertDatabaseValues([]);
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_scroll_hashchange() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await SpecialPowers.spawn(browser, [], function () {
+ const heading = content.document.getElementById("heading");
+ heading.focus();
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ await ContentTask.spawn(browser, TEST_URL + "#foo", url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await waitForScrollEvent(browser, () =>
+ EventUtils.synthesizeKey("KEY_ArrowDown")
+ );
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ scrollingDistanceIsGreaterThan: 0,
+ scrollingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_typing.js b/browser/components/places/tests/browser/interactions/browser_interactions_typing.js
new file mode 100644
index 0000000000..99269c3265
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_typing.js
@@ -0,0 +1,410 @@
+/* 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 reporting of typing interactions.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html";
+const TEST_URL2 = "https://example.com/browser";
+const TEST_URL3 =
+ "https://example.com/browser/browser/base/content/test/contextMenu/subtst_contextmenu_input.html";
+
+const sentence = "The quick brown fox jumps over the lazy dog.";
+const sentenceFragments = [
+ "The quick",
+ " brown fox",
+ " jumps over the lazy dog.",
+];
+const longSentence =
+ "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas ut purus a libero cursus scelerisque. In hac habitasse platea dictumst. Quisque posuere ante sed consequat volutpat.";
+
+// For tests where it matters reduces the maximum time between keypresses to a length that we can
+// afford to delay the test by.
+const PREF_TYPING_DELAY = "browser.places.interactions.typing_timeout_ms";
+const POST_TYPING_DELAY = 150;
+
+async function sendTextToInput(browser, text) {
+ await SpecialPowers.spawn(browser, [], function () {
+ const input = content.document.querySelector(
+ "#form1 > input[name='search']"
+ );
+ input.focus();
+ input.value = ""; // Reset to later verify that the provided text matches the value
+ });
+
+ EventUtils.sendString(text);
+
+ await SpecialPowers.spawn(browser, [{ text }], async function (args) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#form1 > input[name='search']").value ==
+ args.text,
+ "Text has been set on input"
+ );
+ });
+}
+
+add_task(async function test_load_and_navigate_away_no_keypresses() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_load_type_and_navigate_away() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_no_typing_close_tab() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {});
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+});
+
+add_task(async function test_typing_close_tab() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+});
+
+add_task(async function test_single_key_typing_and_delay() {
+ await Interactions.reset();
+
+ Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY);
+
+ try {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ // Single keystrokes with a delay between each, are not considered typing
+ const text = ["T", "h", "e"];
+
+ for (let i = 0; i < text.length; i++) {
+ await sendTextToInput(browser, text[i]);
+
+ // We do need to wait here because typing is defined as a series of keystrokes followed by a delay.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2));
+ }
+ });
+ } finally {
+ Services.prefs.clearUserPref(PREF_TYPING_DELAY);
+ }
+
+ // Since we typed single keys with delays between each, there should be no typing added to the database
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+});
+
+add_task(async function test_double_key_typing_and_delay() {
+ await Interactions.reset();
+
+ // Test three 2-key typing bursts.
+ const text = ["Ab", "cd", "ef"];
+
+ const testStartTime = Cu.now();
+
+ Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY);
+
+ try {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ for (let i = 0; i < text.length; i++) {
+ await sendTextToInput(browser, text[i]);
+
+ // We do need to wait here because typing is defined as a series of keystrokes followed by a delay.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2));
+ }
+ });
+ } finally {
+ Services.prefs.clearUserPref(PREF_TYPING_DELAY);
+ }
+
+ // All keys should be recorded
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: text.reduce(
+ (accumulator, current) => accumulator + current.length,
+ 0
+ ),
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ ]);
+});
+
+add_task(async function test_typing_and_delay() {
+ await Interactions.reset();
+
+ const testStartTime = Cu.now();
+
+ Services.prefs.setIntPref(PREF_TYPING_DELAY, POST_TYPING_DELAY);
+
+ try {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ for (let i = 0; i < sentenceFragments.length; i++) {
+ await sendTextToInput(browser, sentenceFragments[i]);
+
+ // We do need to wait here because typing is defined as a series of keystrokes followed by a delay.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(r => setTimeout(r, POST_TYPING_DELAY * 2));
+ }
+ });
+ } finally {
+ Services.prefs.clearUserPref(PREF_TYPING_DELAY);
+ }
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments.reduce(
+ (accumulator, current) => accumulator + current.length,
+ 0
+ ),
+ typingTimeIsGreaterThan: 0,
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ ]);
+});
+
+add_task(async function test_typing_and_reload() {
+ await Interactions.reset();
+
+ const testStartTime = Cu.now();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentenceFragments[0]);
+
+ info("reload");
+ browser.reload();
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
+
+ // First typing should have been recorded
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments[0].length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ await sendTextToInput(browser, sentenceFragments[1]);
+
+ info("reload");
+ browser.reload();
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
+
+ // Second typing should have been recorded
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments[0].length,
+ typingTimeIsGreaterThan: 0,
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ {
+ url: TEST_URL,
+ keypresses: sentenceFragments[1].length,
+ typingTimeIsGreaterThan: 0,
+ typingTimeIsLessThan: Cu.now() - testStartTime,
+ },
+ ]);
+ });
+}).skip(); // Bug 1749328 - intermittent failure: dropping the 2nd interaction after the 2nd reload
+
+add_task(async function test_switch_tabs_no_typing() {
+ await Interactions.reset();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2);
+
+ info("Switch to second tab");
+ gBrowser.selectedTab = tab2;
+
+ // Only the interaction of the first tab should be recorded so far, and with no typing
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+add_task(async function test_typing_switch_tabs() {
+ await Interactions.reset();
+
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ await sendTextToInput(tab1.linkedBrowser, sentence);
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL3);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL3);
+
+ info("Switch to second tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+
+ // Only the interaction of the first tab should be recorded so far
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ const tab1TyingTime = await getDatabaseValue(TEST_URL, "typingTime");
+
+ info("Switch back to first tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ // The interaction of the second tab should now be recorded (no typing)
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ exactTypingTime: tab1TyingTime,
+ },
+ {
+ url: TEST_URL3,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ info("Switch back to the second tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ exactTypingTime: tab1TyingTime,
+ },
+ {
+ url: TEST_URL3,
+ keypresses: 0,
+ exactTypingTime: 0,
+ },
+ ]);
+
+ // Typing into the second tab
+ await SpecialPowers.spawn(tab2.linkedBrowser, [], function () {
+ const input = content.document.getElementById("input_text");
+ input.focus();
+ });
+ await EventUtils.sendString(longSentence);
+ await TestUtils.waitForTick();
+
+ info("Switch back to first tab");
+ await BrowserTestUtils.switchTab(gBrowser, tab1);
+
+ // The interaction of the second tab should now also be recorded (with typing)
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ exactTypingTime: tab1TyingTime,
+ },
+ {
+ url: TEST_URL3,
+ keypresses: longSentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js
new file mode 100644
index 0000000000..e64050ec1d
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js
@@ -0,0 +1,171 @@
+/* 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 reporting of typing interactions after DOM history API usage.
+ */
+
+"use strict";
+
+const TEST_URL =
+ "https://example.com/browser/browser/components/places/tests/browser/keyword_form.html";
+const TEST_URL2 = "https://example.com/browser";
+
+const sentence = "The quick brown fox jumps over the lazy dog.";
+
+async function sendTextToInput(browser, text) {
+ await SpecialPowers.spawn(browser, [], function () {
+ const input = content.document.querySelector(
+ "#form1 > input[name='search']"
+ );
+ input.focus();
+ input.value = ""; // Reset to later verify that the provided text matches the value
+ });
+
+ EventUtils.sendString(text);
+
+ await SpecialPowers.spawn(browser, [{ text }], async function (args) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.document.querySelector("#form1 > input[name='search']").value ==
+ args.text,
+ "Text has been set on input"
+ );
+ });
+}
+
+add_task(async function test_typing_pushState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_pushState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length * 2,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_replaceState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ {
+ url: TEST_URL2,
+ keypresses: sentence.length,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_replaceState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length * 2,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_typing_hashchange() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ await sendTextToInput(browser, sentence);
+
+ await ContentTask.spawn(browser, TEST_URL + "#foo", url => {
+ content.location = url;
+ });
+
+ await sendTextToInput(browser, sentence);
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ keypresses: sentence.length * 2,
+ typingTimeIsGreaterThan: 0,
+ },
+ ]);
+ });
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js b/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js
new file mode 100644
index 0000000000..278ae10228
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js
@@ -0,0 +1,410 @@
+/* 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 page view time recording for interactions.
+ */
+
+const TEST_URL = "https://example.com/";
+const TEST_URL2 = "https://example.com/browser";
+const TEST_URL3 = "https://example.com/browser/browser";
+const TEST_URL4 = "https://example.com/browser/browser/components";
+
+add_task(async function test_interactions_simple_load_and_navigate_away() {
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.startLoadingURIString(browser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2);
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:blank");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:blank");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_interactions_simple_load_and_change_to_non_http() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.startLoadingURIString(browser, "about:support");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:support");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ });
+});
+
+add_task(async function test_interactions_close_tab() {
+ await Interactions.reset();
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_background_tab() {
+ await Interactions.reset();
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2);
+
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.removeTab(tab2);
+
+ // This is checking a non-action, so let the event queue clear to try and
+ // detect any unexpected database writes. We wait for a few ticks to
+ // make it more likely. however if this fails it may show up as an
+ // intermittent.
+ await TestUtils.waitForTick();
+ await TestUtils.waitForTick();
+ await TestUtils.waitForTick();
+
+ await assertDatabaseValues([]);
+
+ BrowserTestUtils.removeTab(tab1);
+
+ // Only the interaction in the visible tab should have been recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_switch_tabs() {
+ await Interactions.reset();
+ let tab1 = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ let tab2 = BrowserTestUtils.addTab(gBrowser, TEST_URL2);
+ await BrowserTestUtils.browserLoaded(tab2.linkedBrowser, false, TEST_URL2);
+
+ info("Switch to second tab");
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+ gBrowser.selectedTab = tab2;
+
+ // Only the interaction of the first tab should be recorded so far.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let tab1ViewTime = await getDatabaseValue(TEST_URL, "totalViewTime");
+
+ info("Switch back to first tab");
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ gBrowser.selectedTab = tab1;
+
+ // The interaction of the second tab should now be recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: tab1ViewTime,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+
+ info("Switch to second tab again");
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ gBrowser.selectedTab = tab2;
+
+ // The interaction of the second tab should now be recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: tab1ViewTime + 30000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tab1);
+ BrowserTestUtils.removeTab(tab2);
+});
+
+add_task(async function test_interactions_switch_windows() {
+ await Interactions.reset();
+
+ // Open a tab in the first window.
+ let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ // and then load the second window.
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ BrowserTestUtils.startLoadingURIString(
+ otherWin.gBrowser.selectedBrowser,
+ TEST_URL2
+ );
+ await BrowserTestUtils.browserLoaded(
+ otherWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL2
+ );
+ await SimpleTest.promiseFocus(otherWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let originalWindowViewTime = await getDatabaseValue(
+ TEST_URL,
+ "totalViewTime"
+ );
+
+ info("Switch back to original window");
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ await SimpleTest.promiseFocus(window);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: originalWindowViewTime,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+ let newWindowViewTime = await getDatabaseValue(TEST_URL2, "totalViewTime");
+
+ info("Switch back to new window");
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ await SimpleTest.promiseFocus(otherWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: originalWindowViewTime + 30000,
+ },
+ {
+ url: TEST_URL2,
+ exactTotalViewTime: newWindowViewTime,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tabInOriginalWindow);
+ await BrowserTestUtils.closeWindow(otherWin);
+});
+
+add_task(async function test_interactions_loading_in_unfocused_windows() {
+ await Interactions.reset();
+
+ let otherWin = await BrowserTestUtils.openNewBrowserWindow();
+
+ BrowserTestUtils.startLoadingURIString(
+ otherWin.gBrowser.selectedBrowser,
+ TEST_URL
+ );
+ await BrowserTestUtils.browserLoaded(
+ otherWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL
+ );
+
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ BrowserTestUtils.startLoadingURIString(
+ otherWin.gBrowser.selectedBrowser,
+ TEST_URL2
+ );
+ await BrowserTestUtils.browserLoaded(
+ otherWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL2
+ );
+
+ // Only the interaction of the first tab should be recorded so far.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let newWindowViewTime = await getDatabaseValue(TEST_URL, "totalViewTime");
+
+ // Open a tab in the background window, and then navigate somewhere else,
+ // this should not record an intereaction.
+ let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL3,
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ BrowserTestUtils.startLoadingURIString(
+ tabInOriginalWindow.linkedBrowser,
+ TEST_URL4
+ );
+ await BrowserTestUtils.browserLoaded(
+ tabInOriginalWindow.linkedBrowser,
+ false,
+ TEST_URL4
+ );
+
+ // Only the interaction of the first tab should be recorded so far.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: newWindowViewTime,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tabInOriginalWindow);
+ await BrowserTestUtils.closeWindow(otherWin);
+});
+
+add_task(async function test_interactions_private_browsing() {
+ await Interactions.reset();
+
+ // Open a tab in the first window.
+ let tabInOriginalWindow = await BrowserTestUtils.openNewForegroundTab({
+ gBrowser,
+ url: TEST_URL,
+ });
+
+ // and then load the second window.
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ let privateWin = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+
+ BrowserTestUtils.startLoadingURIString(
+ privateWin.gBrowser.selectedBrowser,
+ TEST_URL2
+ );
+ await BrowserTestUtils.browserLoaded(
+ privateWin.gBrowser.selectedBrowser,
+ false,
+ TEST_URL2
+ );
+ await SimpleTest.promiseFocus(privateWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ let originalWindowViewTime = await getDatabaseValue(
+ TEST_URL,
+ "totalViewTime"
+ );
+
+ info("Switch back to original window");
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ // As we're checking for a non-action, wait for the focus to have definitely
+ // completed, and then let the event queues clear.
+ await SimpleTest.promiseFocus(window);
+ await TestUtils.waitForTick();
+
+ // The private window site should not be recorded.
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: originalWindowViewTime,
+ },
+ ]);
+
+ info("Switch back to new window");
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ await SimpleTest.promiseFocus(privateWin);
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: originalWindowViewTime + 30000,
+ },
+ ]);
+
+ BrowserTestUtils.removeTab(tabInOriginalWindow);
+ await BrowserTestUtils.closeWindow(privateWin);
+});
+
+add_task(async function test_interactions_idle() {
+ await Interactions.reset();
+ let lastViewTime;
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ Interactions.observe(null, "idle", "");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ ]);
+ lastViewTime = await getDatabaseValue(TEST_URL, "totalViewTime");
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+
+ Interactions.observe(null, "active", "");
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ exactTotalViewTime: lastViewTime,
+ },
+ ]);
+
+ Interactions._pageViewStartTime = Cu.now() - 30000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: lastViewTime + 30000,
+ maxViewTime: lastViewTime + 30000 + 10000,
+ },
+ ]);
+});
diff --git a/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js b/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js
new file mode 100644
index 0000000000..857a846417
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js
@@ -0,0 +1,123 @@
+/* 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 page view time recording for interactions after DOM history API usage.
+ */
+
+const TEST_URL = "https://example.com/";
+const TEST_URL2 = "https://example.com/browser";
+
+add_task(async function test_interactions_pushState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_pushState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.pushState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_replaceState() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL2, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 10000,
+ },
+ {
+ url: TEST_URL2,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_replaceState_sameUrl() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL, url => {
+ content.history.replaceState(null, "", url);
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
+
+add_task(async function test_interactions_hashchange() {
+ await Interactions.reset();
+
+ await BrowserTestUtils.withNewTab(TEST_URL, async browser => {
+ Interactions._pageViewStartTime = Cu.now() - 10000;
+
+ await ContentTask.spawn(browser, TEST_URL + "#foo", url => {
+ content.location = url;
+ });
+
+ Interactions._pageViewStartTime = Cu.now() - 20000;
+ });
+
+ await assertDatabaseValues([
+ {
+ url: TEST_URL,
+ totalViewTime: 20000,
+ },
+ ]);
+});
diff --git a/browser/components/places/tests/browser/interactions/head.js b/browser/components/places/tests/browser/interactions/head.js
new file mode 100644
index 0000000000..92a096cc5f
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/head.js
@@ -0,0 +1,201 @@
+/* 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 { Interactions } = ChromeUtils.importESModule(
+ "resource:///modules/Interactions.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+ sinon: "resource://testing-common/Sinon.sys.mjs",
+});
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "pageViewIdleTime",
+ "browser.places.interactions.pageViewIdleTime",
+ 60
+);
+
+add_setup(async function global_setup() {
+ // Disable idle management because it interacts with our code, causing
+ // unexpected intermittent failures, we'll fake idle notifications when
+ // we need to test it.
+ let idleService = Cc["@mozilla.org/widget/useridleservice;1"].getService(
+ Ci.nsIUserIdleService
+ );
+ idleService.removeIdleObserver(Interactions, pageViewIdleTime);
+ registerCleanupFunction(() => {
+ idleService.addIdleObserver(Interactions, pageViewIdleTime);
+ });
+
+ // Clean interactions for each test.
+ await Interactions.reset();
+ registerCleanupFunction(async () => {
+ await Interactions.reset();
+ });
+});
+
+/**
+ * Ensures that a list of interactions have been permanently stored.
+ *
+ * @param {Array} expected list of interactions to be found.
+ * @param {boolean} [dontFlush] Avoid flushing pending data.
+ */
+async function assertDatabaseValues(expected, { dontFlush = false } = {}) {
+ await Interactions.interactionUpdatePromise;
+ if (!dontFlush) {
+ await Interactions.store.flush();
+ }
+
+ let interactions = await PlacesUtils.withConnectionWrapper(
+ "head.js::assertDatabaseValues",
+ async db => {
+ let rows = await db.execute(`
+ SELECT h.url AS url, h2.url as referrer_url, total_view_time, key_presses, typing_time, scrolling_time, scrolling_distance
+ FROM moz_places_metadata m
+ JOIN moz_places h ON h.id = m.place_id
+ LEFT JOIN moz_places h2 ON h2.id = m.referrer_place_id
+ ORDER BY created_at ASC
+ `);
+ return rows.map(r => ({
+ url: r.getResultByName("url"),
+ referrerUrl: r.getResultByName("referrer_url"),
+ keypresses: r.getResultByName("key_presses"),
+ typingTime: r.getResultByName("typing_time"),
+ totalViewTime: r.getResultByName("total_view_time"),
+ scrollingTime: r.getResultByName("scrolling_time"),
+ scrollingDistance: r.getResultByName("scrolling_distance"),
+ }));
+ }
+ );
+ info(
+ `Found ${interactions.length} interactions:\n ${JSON.stringify(
+ interactions
+ )}`
+ );
+ Assert.equal(
+ interactions.length,
+ expected.length,
+ "Found the expected number of entries"
+ );
+ for (let i = 0; i < Math.min(expected.length, interactions.length); i++) {
+ let actual = interactions[i];
+ Assert.equal(
+ actual.url,
+ expected[i].url,
+ "Should have saved the page into the database"
+ );
+
+ if (expected[i].exactTotalViewTime != undefined) {
+ Assert.equal(
+ actual.totalViewTime,
+ expected[i].exactTotalViewTime,
+ "Should have kept the exact time."
+ );
+ } else if (expected[i].totalViewTime != undefined) {
+ Assert.greaterOrEqual(
+ actual.totalViewTime,
+ expected[i].totalViewTime,
+ "Should have stored the interaction time"
+ );
+ }
+
+ if (expected[i].maxViewTime != undefined) {
+ Assert.less(
+ actual.totalViewTime,
+ expected[i].maxViewTime,
+ "Should have recorded an interaction below the maximum expected"
+ );
+ }
+
+ if (expected[i].keypresses != undefined) {
+ Assert.equal(
+ actual.keypresses,
+ expected[i].keypresses,
+ "Should have saved the keypresses into the database"
+ );
+ }
+
+ if (expected[i].exactTypingTime != undefined) {
+ Assert.equal(
+ actual.typingTime,
+ expected[i].exactTypingTime,
+ "Should have stored the exact typing time."
+ );
+ } else if (expected[i].typingTimeIsGreaterThan != undefined) {
+ Assert.greater(
+ actual.typingTime,
+ expected[i].typingTimeIsGreaterThan,
+ "Should have stored at least this amount of typing time."
+ );
+ } else if (expected[i].typingTimeIsLessThan != undefined) {
+ Assert.less(
+ actual.typingTime,
+ expected[i].typingTimeIsLessThan,
+ "Should have stored less than this amount of typing time."
+ );
+ }
+
+ if (expected[i].exactScrollingDistance != undefined) {
+ Assert.equal(
+ actual.scrollingDistance,
+ expected[i].exactScrollingDistance,
+ "Should have scrolled by exactly least this distance"
+ );
+ } else if (expected[i].exactScrollingTime != undefined) {
+ Assert.greater(
+ actual.scrollingTime,
+ expected[i].exactScrollingTime,
+ "Should have scrolled for exactly least this duration"
+ );
+ }
+
+ if (expected[i].scrollingDistanceIsGreaterThan != undefined) {
+ Assert.greater(
+ actual.scrollingDistance,
+ expected[i].scrollingDistanceIsGreaterThan,
+ "Should have scrolled by at least this distance"
+ );
+ } else if (expected[i].scrollingTimeIsGreaterThan != undefined) {
+ Assert.greater(
+ actual.scrollingTime,
+ expected[i].scrollingTimeIsGreaterThan,
+ "Should have scrolled for at least this duration"
+ );
+ }
+ }
+}
+
+/**
+ * Ensures that a list of interactions have been permanently stored.
+ *
+ * @param {string} url The url to query.
+ * @param {string} property The property to extract.
+ */
+async function getDatabaseValue(url, property) {
+ await Interactions.store.flush();
+ const PROP_TRANSLATOR = {
+ totalViewTime: "total_view_time",
+ keypresses: "key_presses",
+ typingTime: "typing_time",
+ };
+ property = PROP_TRANSLATOR[property] || property;
+
+ return PlacesUtils.withConnectionWrapper(
+ "head.js::getDatabaseValue",
+ async db => {
+ let rows = await db.execute(
+ `
+ SELECT * FROM moz_places_metadata m
+ JOIN moz_places h ON h.id = m.place_id
+ WHERE url = :url
+ ORDER BY created_at DESC
+ `,
+ { url }
+ );
+ return rows?.[0].getResultByName(property);
+ }
+ );
+}
diff --git a/browser/components/places/tests/browser/interactions/scrolling.html b/browser/components/places/tests/browser/interactions/scrolling.html
new file mode 100644
index 0000000000..ce435097e6
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/scrolling.html
@@ -0,0 +1,121 @@
+<!DOCTYPE HTML>
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+ <style>
+ .div {
+ height: 250px;
+ width: 250px;
+ overflow: auto;
+ }
+
+ .content {
+ height: 800px;
+ width: 2000px;
+ background-color: coral;
+ }
+ </style>
+</head>
+<body>
+
+ <h1 id="heading" >Scrolling interaction tests</h1>
+
+ <br><br><br>
+
+ <div class="div" id="scroll_div" >
+ <div class="content" id="scrollable_div_content" tabindex="-1">
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. <br> Maecenas vitae condimentum erat, vel luctus nulla. <br>Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus. <br>Duis varius eleifend ex. <br>Vivamus tristique faucibus tortor, in commodo felis posuere eu. <br>Sed aliquam tristique enim at volutpat. <br>Phasellus a aliquam quam, ac hendrerit libero. <br>Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus. <br>Ut commodo lobortis egestas. <br>Vivamus vulputate nisi non ligula ullamcorper pulvinar. <br>Phasellus tincidunt leo et venenatis auctor. <br>Proin ac urna et risus ullamcorper viverra. <br>Morbi non nunc at augue tincidunt scelerisque. <br>Proin vel ligula erat. <br>Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non.
+ Donec mattis porttitor consectetur. <br>Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis. <br>Donec a mi mattis, feugiat lectus a, rutrum leo. <br>Suspendisse et tellus urna. <br>Quisque porta ex at efficitur venenatis. <br>Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed. <br>Pellentesque tristique tortor a nisl imperdiet convallis. <br>Curabitur ornare pharetra lacus at luctus. <br>
+ </div>
+ </div>
+
+ <br>
+
+ <br><br>
+
+ <a href="#bottom" id="to_bottom_anchor">click to scroll to bottom</a>
+ <a id="top"></a>
+
+ <br>
+ <iframe title="subframe" src="scrolling_subframe.html" id="subframe"> </iframe>
+
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+
+ <div id="lowerDiv" class="div" >
+ <div class="content"> Scroll inside me!</div>
+ </div>
+
+ <h1 id="middleHeading" > Middle </h1>
+ This is the middle anchor #1<a id="middleAnchor"></a>
+
+ <a href="#top">click to scroll to top</a>
+
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ <br><br><br><br><br><br><br><br><br><br>
+ This is the bottom anchor #3<a id="bottom"></a>
+
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/interactions/scrolling_subframe.html b/browser/components/places/tests/browser/interactions/scrolling_subframe.html
new file mode 100644
index 0000000000..55ad70f295
--- /dev/null
+++ b/browser/components/places/tests/browser/interactions/scrolling_subframe.html
@@ -0,0 +1,25 @@
+<!DOCTYPE HTML>
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+ <style>
+ .div {
+ height: 250px;
+ width: 250px;
+ overflow: auto;
+ }
+ .content {
+ height: 800px;
+ width: 2000px;
+ background-color: coral;
+ }
+ </style>
+</head>
+<body>
+
+ Subframe Content<br>
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. <br> Maecenas vitae condimentum erat, vel luctus nulla. <br>Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus. <br>Duis varius eleifend ex. <br>Vivamus tristique faucibus tortor, in commodo felis posuere eu. <br>Sed aliquam tristique enim at volutpat. <br>Phasellus a aliquam quam, ac hendrerit libero. <br>Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus. <br>Ut commodo lobortis egestas. <br>Vivamus vulputate nisi non ligula ullamcorper pulvinar. <br>Phasellus tincidunt leo et venenatis auctor. <br>Proin ac urna et risus ullamcorper viverra. <br>Morbi non nunc at augue tincidunt scelerisque. <br>Proin vel ligula erat. <br>Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non.
+ Donec mattis porttitor consectetur. <br>Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis. <br>Donec a mi mattis, feugiat lectus a, rutrum leo. <br>Suspendisse et tellus urna. <br>Quisque porta ex at efficitur venenatis. <br>Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed. <br>Pellentesque tristique tortor a nisl imperdiet convallis. <br>Curabitur ornare pharetra lacus at luctus. <br>
+
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/keyword_form.html b/browser/components/places/tests/browser/keyword_form.html
new file mode 100644
index 0000000000..a881c0d5ad
--- /dev/null
+++ b/browser/components/places/tests/browser/keyword_form.html
@@ -0,0 +1,17 @@
+<!DOCTYPE HTML>
+
+<html lang="en">
+<head>
+ <meta http-equiv="Content-Type" content="text/html;charset=windows-1252">
+</head>
+<body>
+ <form id="form1" method="POST" action="keyword_form.html">
+ <input type="hidden" name="accenti" value="àèìòù">
+ <input type="text" name="search">
+ </form>
+ <form id="form2" method="POST" action="keyword_form.html">
+ <input type="hidden" name="accenti" value="ùòìèà">
+ <input type="text" name="search">
+ </form>
+</body>
+</html>
diff --git a/browser/components/places/tests/browser/pageopeningwindow.html b/browser/components/places/tests/browser/pageopeningwindow.html
new file mode 100644
index 0000000000..855f67626e
--- /dev/null
+++ b/browser/components/places/tests/browser/pageopeningwindow.html
@@ -0,0 +1,10 @@
+<meta charset="UTF-8">
+Hi, I was opened via a <script>
+document.write(location.search ?
+ "popup call from the opened window... uh oh, that shouldn't happen!" :
+ "bookmarklet, and I will open a new window myself.");</script><br>
+<script>
+ if (!location.search) {
+ open(location.href + "?donotopen=true", "_blank");
+ }
+</script>
diff --git a/browser/components/places/tests/browser/sidebarpanels_click_test_page.html b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html
new file mode 100644
index 0000000000..c73eaa5403
--- /dev/null
+++ b/browser/components/places/tests/browser/sidebarpanels_click_test_page.html
@@ -0,0 +1,7 @@
+<html>
+<head>
+ <title>browser_sidebarpanels_click.js test page</title>
+</head>
+<body onload="alert('test');">
+</body>
+</html>
diff --git a/browser/components/places/tests/chrome/chrome.toml b/browser/components/places/tests/chrome/chrome.toml
new file mode 100644
index 0000000000..ca953fe898
--- /dev/null
+++ b/browser/components/places/tests/chrome/chrome.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+support-files = ["head.js"]
+
+["test_0_bug510634.xhtml"]
+
+["test_bug549192.xhtml"]
+
+["test_bug549491.xhtml"]
+
+["test_bug1163447_selectItems_through_shortcut.xhtml"]
+
+["test_selectItems_on_nested_tree.xhtml"]
+
+["test_treeview_date.xhtml"]
diff --git a/browser/components/places/tests/chrome/head.js b/browser/components/places/tests/chrome/head.js
new file mode 100644
index 0000000000..6a19fd89d3
--- /dev/null
+++ b/browser/components/places/tests/chrome/head.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Services.scriptloader.loadSubScript(
+ "chrome://global/content/globalOverlay.js",
+ this
+);
+Services.scriptloader.loadSubScript(
+ "chrome://browser/content/utilityOverlay.js",
+ this
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ BrowserTestUtils: "resource://testing-common/BrowserTestUtils.sys.mjs",
+ PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs",
+});
+
+ChromeUtils.defineESModuleGetters(window, {
+ PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ PlacesTransactions: "resource://gre/modules/PlacesTransactions.sys.mjs",
+});
+
+var { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ window,
+ ["PlacesTreeView"],
+ "chrome://browser/content/places/treeView.js"
+);
+XPCOMUtils.defineLazyScriptGetter(
+ window,
+ ["PlacesInsertionPoint", "PlacesController", "PlacesControllerDragHelper"],
+ "chrome://browser/content/places/controller.js"
+);
diff --git a/browser/components/places/tests/chrome/test_0_bug510634.xhtml b/browser/components/places/tests/chrome/test_0_bug510634.xhtml
new file mode 100644
index 0000000000..8cac56ff7c
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_0_bug510634.xhtml
@@ -0,0 +1,100 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="510634: Wrong icons on bookmarks sidebar"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/places-tree.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ is="places-tree"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 510634 - Wrong icons on bookmarks sidebar
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=510634
+ *
+ * Ensures that properties for special queries are set on their tree nodes.
+ */
+
+ SimpleTest.waitForExplicitFinish();
+
+ function runTest() {
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = `place:type=${Ci.nsINavHistoryQueryOptions.RESULTS_AS_LEFT_PANE_QUERY}&excludeItems=1&expandQueries=0`;
+
+ // The query-property is set on the title column for each row.
+ let titleColumn = tree.columns.getColumnAt(0);
+
+ // Open All Bookmarks
+ tree.selectItems([PlacesUtils.virtualAllBookmarksGuid]);
+ PlacesUtils.asContainer(tree.selectedNode).containerOpen = true;
+ is(tree.selectedNode.uri,
+ "place:type=" + Ci.nsINavHistoryQueryOptions.RESULTS_AS_ROOTS_QUERY,
+ "Opened All Bookmarks");
+
+ const topLevelGuids = [
+ PlacesUtils.virtualHistoryGuid,
+ PlacesUtils.virtualDownloadsGuid,
+ PlacesUtils.virtualTagsGuid,
+ PlacesUtils.virtualAllBookmarksGuid
+ ];
+
+ for (let queryName of topLevelGuids) {
+ let found = false;
+ for (let i = 0; i < tree.view.rowCount && !found; i++) {
+ let rowProperties = tree.view.getCellProperties(i, titleColumn).split(" ");
+ found = rowProperties.includes("OrganizerQuery_" + queryName);
+ }
+ ok(found, `OrganizerQuery_${queryName} is set`);
+ }
+
+ const folderGuids = [
+ PlacesUtils.bookmarks.toolbarGuid,
+ PlacesUtils.bookmarks.menuGuid,
+ PlacesUtils.bookmarks.unfiledGuid,
+ ];
+
+ for (let guid of folderGuids) {
+ let found = false;
+ for (let i = 0; i < tree.view.rowCount && !found; i++) {
+ let rowProperties = tree.view.getCellProperties(i, titleColumn).split(" ");
+ found = rowProperties.includes("queryFolder_" + guid);
+ }
+ ok(found, `queryFolder_${guid} is set`);
+ }
+
+ // Close the root node
+ tree.result.root.containerOpen = false;
+
+ SimpleTest.finish();
+ }
+
+ ]]>
+ </script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml
new file mode 100644
index 0000000000..03f5d92572
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml
@@ -0,0 +1,88 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="1163447: selectItems in Places no longer selects items within Toolbar or Sidebar folders"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/places-tree.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ is="places-tree"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+
+ /**
+ * Bug 1163447: places-tree should be able to select an item within the toolbar, and
+ * unfiled bookmarks. Yet not follow recursive folder-shortcuts infinitely.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ let bmu = PlacesUtils.bookmarks;
+
+ await bmu.insert({
+ parentGuid: bmu.toolbarGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_BOOKMARK,
+ url: `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`,
+ title: "shortcut to self - causing infinite recursion if not handled properly"
+ });
+
+ await bmu.insert({
+ parentGuid: bmu.toolbarGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_BOOKMARK,
+ url: `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`,
+ title: "shortcut to unfiled, within toolbar"
+ });
+
+ let folder = await bmu.insert({
+ parentGuid: bmu.unfiledGuid,
+ index: bmu.DEFAULT_INDEX,
+ type: bmu.TYPE_FOLDER,
+ title: "folder within unfiled"
+ });
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`;
+
+ // Select the folder via the selectItems(folder.guid) API being tested
+ tree.selectItems([folder.guid]);
+
+ is(tree.selectedNode && tree.selectedNode.bookmarkGuid, folder.guid, "The node was selected through the shortcut");
+
+ // Cleanup
+ await bmu.eraseEverything();
+
+ })().catch(err => {
+ ok(false, `Uncaught error: ${err}`);
+ }).then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug549192.xhtml b/browser/components/places/tests/chrome/test_bug549192.xhtml
new file mode 100644
index 0000000000..9f00e8b9c5
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug549192.xhtml
@@ -0,0 +1,130 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549192: History view not updated after deleting entry"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/places-tree.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ is="places-tree"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Bug 874407
+ * Ensures that history views are updated properly after visits.
+ * Bug 549192
+ * Ensures that history views are updated after deleting entries.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ await PlacesUtils.history.clear();
+
+ // Add some visits.
+ let timeInMicroseconds = PlacesUtils.toPRTime(Date.now() - 10000);
+
+ function newTimeInMicroseconds() {
+ timeInMicroseconds = timeInMicroseconds + 1000;
+ return timeInMicroseconds;
+ }
+
+ const transition = PlacesUtils.history.TRANSITIONS.TYPED;
+ let places =
+ [{ uri: Services.io.newURI("http://example.tld/"),
+ visitDate: newTimeInMicroseconds(), transition },
+ { uri: Services.io.newURI("http://example2.tld/"),
+ visitDate: newTimeInMicroseconds(), transition },
+ { uri: Services.io.newURI("http://example3.tld/"),
+ visitDate: newTimeInMicroseconds(), transition }];
+
+ await PlacesTestUtils.addVisits(places);
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ opts.sortingMode = opts.SORT_BY_DATE_DESCENDING;
+ let queryURI = PlacesUtils.history.queryToQueryString(query, opts);
+
+ // Setup the places tree contents.
+ var tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check them.
+ let treeView = tree.view;
+ let selection = treeView.selection;
+ let rc = treeView.rowCount;
+
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ is(rc, 3, "Found expected number of rows.");
+
+ // First check live-update of the view when adding visits.
+ places.forEach(place => place.visitDate = newTimeInMicroseconds());
+ await PlacesTestUtils.addVisits(places);
+
+ for (let i = 0; i < rc; i++) {
+ selection.select(i);
+ let node = tree.selectedNode;
+ is(node.uri, places[rc - i - 1].uri.spec,
+ "Found expected node at position " + i + ".");
+ }
+
+ // Now remove the pages and verify live-update again.
+ for (let i = 0; i < rc; i++) {
+ selection.select(0);
+ let node = tree.selectedNode;
+
+ const promiseRemoved = PlacesTestUtils.waitForNotification(
+ "page-removed",
+ events => events[0].url === node.uri
+ );
+
+ tree.controller.remove();
+
+ const removeEvents = await promiseRemoved;
+ ok(
+ removeEvents[0].isRemovedFromStore,
+ "isRemovedFromStore should be true"
+ );
+ ok(treeView.treeIndexForNode(node) == -1, node.uri + " removed.");
+ is(treeView.rowCount, rc - i - 1, "Rows count decreased");
+ }
+
+ // Cleanup.
+ await PlacesUtils.history.clear();
+ })().then(() => SimpleTest.finish());
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_bug549491.xhtml b/browser/components/places/tests/chrome/test_bug549491.xhtml
new file mode 100644
index 0000000000..03fee4cc06
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_bug549491.xhtml
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549491: 'The root node is never visible' exception when details of the root node are modified "
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/places-tree.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ is="places-tree"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Date" anonid="date" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Bug 549491
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=549491
+ *
+ * Ensures that changing the details of places tree's root-node doesn't
+ * throw.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ await PlacesUtils.history.clear();
+
+ await PlacesTestUtils.addVisits({
+ uri: Services.io.newURI("http://example.tld/"),
+ transition: PlacesUtils.history.TRANSITION_TYPED
+ });
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ let queryURI = PlacesUtils.history.queryToQueryString(query, opts);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ let rootNode = tree.result.root;
+ let obs = tree.view.QueryInterface(Ci.nsINavHistoryResultObserver);
+ obs.nodeHistoryDetailsChanged(rootNode, rootNode.time, rootNode.accessCount);
+ obs.nodeTitleChanged(rootNode, rootNode.title);
+ ok(true, "No exceptions thrown");
+
+ // Cleanup.
+ await PlacesUtils.history.clear();
+ })().then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml
new file mode 100644
index 0000000000..6dc2d33041
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml
@@ -0,0 +1,85 @@
+<?xml version="1.0"?>
+
+<!--
+ Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/licenses/publicdomain/
+ -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="549192: History view not updated after deleting entry"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/places-tree.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ is="places-tree"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript"><![CDATA[
+ /**
+ * Ensure that selectItems doesn't recurse infinitely in nested trees.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ (async function() {
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`,
+ title: "shortcut"
+ });
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: `place:parent=${PlacesUtils.bookmarks.unfiledGuid}&maxResults=10`,
+ title: "query"
+ });
+
+ let folder = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_FOLDER,
+ title: "folder"
+ });
+
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: folder.guid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://www.mozilla.org/",
+ title: "bookmark"
+ });
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = `place:parent=${PlacesUtils.bookmarks.unfiledGuid}`;
+
+ // Select the last bookmark.
+ tree.selectItems([bm.guid]);
+ is (tree.selectedNode.bookmarkGuid, bm.guid, "The right node was selected");
+ })().then(SimpleTest.finish);
+ }
+ ]]></script>
+</window>
diff --git a/browser/components/places/tests/chrome/test_treeview_date.xhtml b/browser/components/places/tests/chrome/test_treeview_date.xhtml
new file mode 100644
index 0000000000..8a7853194d
--- /dev/null
+++ b/browser/components/places/tests/chrome/test_treeview_date.xhtml
@@ -0,0 +1,159 @@
+<?xml version="1.0"?>
+
+<!-- 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/. -->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<?xml-stylesheet href="chrome://browser/content/places/places.css"?>
+<?xml-stylesheet href="chrome://browser/skin/places/tree-icons.css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="435322: Places tree view's formatting"
+ onload="runTest();">
+
+ <script type="application/javascript"
+ src="chrome://browser/content/places/places-tree.js"/>
+ <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+ <script type="application/javascript" src="head.js" />
+
+ <body xmlns="http://www.w3.org/1999/xhtml" />
+
+ <tree id="tree"
+ is="places-tree"
+ flatList="true"
+ flex="1">
+ <treecols>
+ <treecol label="Title" id="title" anonid="title" primary="true" style="order: 1;" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Tags" id="tags" anonid="tags" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Url" id="url" anonid="url" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Date" id="date" anonid="date" flex="1"/>
+ <splitter class="tree-splitter"/>
+ <treecol label="Visit Count" id="visitCount" anonid="visitCount" flex="1"/>
+ </treecols>
+ <treechildren flex="1"/>
+ </tree>
+
+ <script type="application/javascript">
+ <![CDATA[
+
+ /**
+ * Bug 435322
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=435322
+ *
+ * Ensures that date in places treeviews is correctly formatted.
+ */
+
+ function runTest() {
+ SimpleTest.waitForExplicitFinish();
+
+ function uri(spec) {
+ return Services.io.newURI(spec);
+ }
+
+ (async function() {
+ await PlacesUtils.history.clear();
+
+ let midnight = new Date();
+ midnight.setHours(0);
+ midnight.setMinutes(0);
+ midnight.setSeconds(0);
+ midnight.setMilliseconds(0);
+
+ // Add a visit 1ms before midnight, a visit at midnight, and
+ // a visit 1ms after midnight.
+ await PlacesTestUtils.addVisits([
+ {uri: uri("http://before.midnight.com/"),
+ visitDate: (midnight.getTime() - 1) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED},
+ {uri: uri("http://at.midnight.com/"),
+ visitDate: (midnight.getTime()) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED},
+ {uri: uri("http://after.midnight.com/"),
+ visitDate: (midnight.getTime() + 1) * 1000,
+ transition: PlacesUtils.history.TRANSITION_TYPED}
+ ]);
+
+ // add a bookmark to the midnight visit
+ let bm = await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ url: "http://at.midnight.com/",
+ title: "A bookmark at midnight",
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK
+ });
+
+ // Make a history query.
+ let query = PlacesUtils.history.getNewQuery();
+ let opts = PlacesUtils.history.getNewQueryOptions();
+ let queryURI = PlacesUtils.history.queryToQueryString(query, opts);
+
+ // Setup the places tree contents.
+ let tree = document.getElementById("tree");
+ tree.place = queryURI;
+
+ // loop through the rows and check formatting
+ let treeView = tree.view;
+ let rc = treeView.rowCount;
+ ok(rc >= 3, "Rows found");
+ let columns = tree.columns;
+ ok(columns.count > 0, "Columns found");
+ for (let r = 0; r < rc; r++) {
+ let node = treeView.nodeForTreeIndex(r);
+ ok(node, "Places node found");
+ for (let ci = 0; ci < columns.count; ci++) {
+ let c = columns.getColumnAt(ci);
+ let text = treeView.getCellText(r, c);
+ switch (c.element.getAttribute("anonid")) {
+ case "title":
+ // The title can differ, we did not set any title so we would
+ // expect null, but in such a case the view will generate a title
+ // through PlacesUIUtils.getBestTitle.
+ if (node.title)
+ is(text, node.title, "Title is correct");
+ break;
+ case "url":
+ is(text, node.uri, "Uri is correct");
+ break;
+ case "date":
+ let timeObj = new Date(node.time / 1000);
+ // Default is short date format.
+ let dtOptions = {
+ dateStyle: "short",
+ timeStyle: "short"
+ };
+
+ // For today's visits we don't show date portion.
+ if (node.uri == "http://at.midnight.com/" ||
+ node.uri == "http://after.midnight.com/") {
+ dtOptions.dateStyle = undefined;
+ } else if (node.uri != "http://before.midnight.com/") {
+ // Avoid to test spurious uris, due to how the test works
+ // a redirecting uri could be put in the tree while we test.
+ break;
+ }
+ let timeStr = new Services.intl.DateTimeFormat(undefined, dtOptions).format(timeObj);
+
+ is(text, timeStr, "Date format is correct");
+ break;
+ case "visitCount":
+ is(text, 1, "Visit count is correct");
+ break;
+ }
+ }
+ }
+
+ // Cleanup.
+ await PlacesUtils.bookmarks.remove(bm.guid);
+ await PlacesUtils.history.clear();
+ })().then(SimpleTest.finish);
+ }
+ ]]>
+ </script>
+</window>
diff --git a/browser/components/places/tests/marionette/manifest.toml b/browser/components/places/tests/marionette/manifest.toml
new file mode 100644
index 0000000000..648f933814
--- /dev/null
+++ b/browser/components/places/tests/marionette/manifest.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+run-if = ["buildapp == 'browser'"]
+
+["test_reopen_from_library.py"]
diff --git a/browser/components/places/tests/marionette/test_reopen_from_library.py b/browser/components/places/tests/marionette/test_reopen_from_library.py
new file mode 100644
index 0000000000..28f6ff3cd6
--- /dev/null
+++ b/browser/components/places/tests/marionette/test_reopen_from_library.py
@@ -0,0 +1,164 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import textwrap
+
+from marionette_driver import Wait
+from marionette_harness import MarionetteTestCase, WindowManagerMixin
+
+
+class TestReopenFromLibrary(WindowManagerMixin, MarionetteTestCase):
+ def setUp(self):
+ super(TestReopenFromLibrary, self).setUp()
+
+ self.original_showForNewBookmarks_pref = self.marionette.get_pref(
+ "browser.bookmarks.editDialog.showForNewBookmarks"
+ )
+ self.original_loadBookmarksInTabs_pref = self.marionette.get_pref(
+ "browser.tabs.loadBookmarksInTabs"
+ )
+
+ self.marionette.set_pref(
+ "browser.bookmarks.editDialog.showForNewBookmarks", False
+ )
+ self.marionette.set_pref("browser.tabs.loadBookmarksInTabs", True)
+
+ def tearDown(self):
+ self.close_all_windows()
+
+ self.marionette.restart(in_app=False, clean=True)
+
+ super(TestReopenFromLibrary, self).tearDown()
+
+ def test_open_bookmark_from_library_with_no_browser_window_open(self):
+ bookmark_url = self.marionette.absolute_url("empty.html")
+ self.marionette.navigate(bookmark_url)
+
+ self.marionette.set_context(self.marionette.CONTEXT_CHROME)
+
+ star_button = self.marionette.find_element("id", "star-button-box")
+
+ # Await for async updates to the star button before clicking on it, as
+ # clicks are ignored during status updates.
+ script = """\
+ return window.BookmarkingUI.status != window.BookmarkingUI.STATUS_UPDATING;
+ """
+ Wait(self.marionette).until(
+ lambda _: self.marionette.execute_script(textwrap.dedent(script)),
+ message="Failed waiting for star updates",
+ )
+
+ star_button.click()
+
+ star_image = self.marionette.find_element("id", "star-button")
+
+ def check(_):
+ return "true" in star_image.get_attribute("starred")
+
+ Wait(self.marionette).until(check, message="Failed to star the page")
+
+ win = self.open_chrome_window(
+ "chrome://browser/content/places/places.xhtml", False
+ )
+
+ self.marionette.close_chrome_window()
+
+ self.marionette.switch_to_window(win)
+
+ # Tree elements can't be accessed in the same way as regular elements,
+ # so this uses some code from the places tests and EventUtils.js to
+ # select the bookmark in the tree and then double-click it.
+ script = """\
+ window.PlacesOrganizer.selectLeftPaneContainerByHierarchy(
+ PlacesUtils.bookmarks.virtualToolbarGuid
+ );
+
+ // Bookmarks may be imported and shift the expected one, so search
+ // for it.
+ let node;
+ for (let i = 1; i < window.ContentTree.view.result.root.childCount; ++i) {
+ node = window.ContentTree.view.view.nodeForTreeIndex(i);
+ if (node.uri.endsWith("empty.html")) {
+ break;
+ }
+ }
+
+ window.ContentTree.view.selectNode(node);
+
+ // Based on synthesizeDblClickOnSelectedTreeCell
+ let tree = window.ContentTree.view;
+
+ if (tree.view.selection.count < 1) {
+ throw new Error("The test node should be successfully selected");
+ }
+ // Get selection rowID.
+ let min = {};
+ let max = {};
+ tree.view.selection.getRangeAt(0, min, max);
+ let rowID = min.value;
+ tree.ensureRowIsVisible(rowID);
+ // Calculate the click coordinates.
+ let rect = tree.getCoordsForCellItem(rowID, tree.columns[0], "text");
+ let x = rect.x + rect.width / 2;
+ let y = rect.y + rect.height / 2;
+ let treeBodyRect = tree.body.getBoundingClientRect();
+ return [treeBodyRect.left + x, treeBodyRect.top + y]
+ """
+
+ position = self.marionette.execute_script(textwrap.dedent(script))
+ # These must be integers for pointer_move
+ x = round(position[0])
+ y = round(position[1])
+
+ self.marionette.actions.sequence(
+ "pointer", "pointer_id", {"pointerType": "mouse"}
+ ).pointer_move(x, y).click().click().perform()
+
+ def window_with_url_open(_):
+ urls_in_windows = self.get_urls_for_windows()
+
+ for urls in urls_in_windows:
+ if bookmark_url in urls:
+ return True
+ return False
+
+ Wait(self.marionette).until(
+ window_with_url_open,
+ message="Failed to open the browser window from the library",
+ )
+
+ # Closes the library window.
+ self.marionette.close_chrome_window()
+
+ def get_urls_for_windows(self):
+ # There's no guarantee that Marionette will return us an
+ # iterator for the opened windows that will match the
+ # order within our window list. Instead, we'll convert
+ # the list of URLs within each open window to a set of
+ # tuples that will allow us to do a direct comparison
+ # while allowing the windows to be in any order.
+ opened_urls = set()
+ for win in self.marionette.chrome_window_handles:
+ urls = tuple(self.get_urls_for_window(win))
+ opened_urls.add(urls)
+
+ return opened_urls
+
+ def get_urls_for_window(self, win):
+ orig_handle = self.marionette.current_chrome_window_handle
+
+ try:
+ self.marionette.switch_to_window(win)
+ return self.marionette.execute_script(
+ """
+ if (!window?.gBrowser) {
+ return [];
+ }
+ return window.gBrowser.tabs.map(tab => {
+ return tab.linkedBrowser.currentURI.spec;
+ });
+ """
+ )
+ finally:
+ self.marionette.switch_to_window(orig_handle)
diff --git a/browser/components/places/tests/unit/bookmarks.glue.html b/browser/components/places/tests/unit/bookmarks.glue.html
new file mode 100644
index 0000000000..07b22e9b3f
--- /dev/null
+++ b/browser/components/places/tests/unit/bookmarks.glue.html
@@ -0,0 +1,16 @@
+<!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>Bookmarks Menu</H1>
+
+<DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ <DT><H3 ADD_DATE="1233157910" LAST_MODIFIED="1233157972" PERSONAL_TOOLBAR_FOLDER="true">Bookmarks Toolbar</H3>
+<DD>Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar
+ <DL><p>
+ <DT><A HREF="http://example.com/" ADD_DATE="1233157972" LAST_MODIFIED="1233157984">example</A>
+ </DL><p>
+</DL><p>
diff --git a/browser/components/places/tests/unit/bookmarks.glue.json b/browser/components/places/tests/unit/bookmarks.glue.json
new file mode 100644
index 0000000000..069f605d29
--- /dev/null
+++ b/browser/components/places/tests/unit/bookmarks.glue.json
@@ -0,0 +1,83 @@
+{
+ "title": "",
+ "id": 1,
+ "dateAdded": 1233157910552624,
+ "lastModified": 1233157955206833,
+ "type": "text/x-moz-place-container",
+ "root": "placesRoot",
+ "children": [
+ {
+ "title": "Bookmarks Menu",
+ "id": 2,
+ "parent": 1,
+ "dateAdded": 1233157910552624,
+ "lastModified": 1233157993171424,
+ "type": "text/x-moz-place-container",
+ "root": "bookmarksMenuFolder",
+ "children": [
+ {
+ "title": "examplejson",
+ "id": 27,
+ "parent": 2,
+ "dateAdded": 1233157972101126,
+ "lastModified": 1233157984999673,
+ "type": "text/x-moz-place",
+ "uri": "http://example.com/"
+ }
+ ]
+ },
+ {
+ "index": 1,
+ "title": "Bookmarks Toolbar",
+ "id": 3,
+ "parent": 1,
+ "dateAdded": 1233157910552624,
+ "lastModified": 1233157972101126,
+ "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": [
+ {
+ "title": "examplejson",
+ "id": 26,
+ "parent": 3,
+ "dateAdded": 1233157972101126,
+ "lastModified": 1233157984999673,
+ "type": "text/x-moz-place",
+ "uri": "http://example.com/"
+ }
+ ]
+ },
+ {
+ "index": 2,
+ "title": "Tags",
+ "id": 4,
+ "parent": 1,
+ "dateAdded": 1233157910552624,
+ "lastModified": 1233157910582667,
+ "type": "text/x-moz-place-container",
+ "root": "tagsFolder",
+ "children": []
+ },
+ {
+ "index": 3,
+ "title": "Other Bookmarks",
+ "id": 5,
+ "parent": 1,
+ "dateAdded": 1233157910552624,
+ "lastModified": 1233157911033315,
+ "type": "text/x-moz-place-container",
+ "root": "unfiledBookmarksFolder",
+ "children": []
+ }
+ ]
+}
diff --git a/browser/components/places/tests/unit/corruptDB.sqlite b/browser/components/places/tests/unit/corruptDB.sqlite
new file mode 100644
index 0000000000..b234246cac
--- /dev/null
+++ b/browser/components/places/tests/unit/corruptDB.sqlite
Binary files differ
diff --git a/browser/components/places/tests/unit/distribution.ini b/browser/components/places/tests/unit/distribution.ini
new file mode 100644
index 0000000000..a25c40fed3
--- /dev/null
+++ b/browser/components/places/tests/unit/distribution.ini
@@ -0,0 +1,30 @@
+# Distribution Configuration File
+# Bug 516444 demo
+
+[Global]
+id=516444
+version=1.0
+about=Test distribution file
+
+[BookmarksToolbar]
+item.1.title=Toolbar Link Before
+item.1.link=https://example.org/toolbar/before/
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
+item.2.type=default
+item.3.type=folder
+item.3.title=Toolbar Folder After
+item.3.folderId=1
+
+[BookmarksMenu]
+item.1.title=Menu Link Before
+item.1.link=https://example.org/menu/before/
+item.1.icon=https://example.org/favicon.png
+item.1.iconData=data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==
+item.2.type=default
+item.3.title=Menu Link After
+item.3.link=https://example.org/menu/after/
+
+[BookmarksFolder-1]
+item.1.title=Toolbar Link Folder
+item.1.link=https://example.org/toolbar/folder/
diff --git a/browser/components/places/tests/unit/head_bookmarks.js b/browser/components/places/tests/unit/head_bookmarks.js
new file mode 100644
index 0000000000..db6bfe8f0f
--- /dev/null
+++ b/browser/components/places/tests/unit/head_bookmarks.js
@@ -0,0 +1,82 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Import common head.
+/* import-globals-from ../../../../../toolkit/components/places/tests/head_common.js */
+var commonFile = do_get_file(
+ "../../../../../toolkit/components/places/tests/head_common.js",
+ false
+);
+if (commonFile) {
+ let uri = Services.io.newFileURI(commonFile);
+ Services.scriptloader.loadSubScript(uri.spec, this);
+}
+
+// Put any other stuff relative to this test folder below.
+
+ChromeUtils.defineESModuleGetters(this, {
+ PlacesUIUtils: "resource:///modules/PlacesUIUtils.sys.mjs",
+});
+
+// Needed by some test that relies on having an app registered.
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo({
+ name: "PlacesTest",
+ ID: "{230de50e-4cd1-11dc-8314-0800200c9a66}",
+ version: "1",
+ platformVersion: "",
+});
+
+// Default bookmarks constants.
+const DEFAULT_BOOKMARKS_ON_TOOLBAR = 1;
+const DEFAULT_BOOKMARKS_ON_MENU = 1;
+
+var createCorruptDB = async function () {
+ let dbPath = PathUtils.join(PathUtils.profileDir, "places.sqlite");
+ await IOUtils.remove(dbPath);
+
+ // Create a corrupt database.
+ let src = PathUtils.join(do_get_cwd().path, "corruptDB.sqlite");
+ await IOUtils.copy(src, dbPath);
+
+ // Check there's a DB now.
+ Assert.ok(await IOUtils.exists(dbPath), "should have a DB now");
+};
+
+const SINGLE_TRY_TIMEOUT = 100;
+const NUMBER_OF_TRIES = 30;
+
+/**
+ * Similar to waitForConditionPromise, but poll for an asynchronous value
+ * every SINGLE_TRY_TIMEOUT ms, for no more than tryCount times.
+ *
+ * @param {Function} promiseFn
+ * A function to generate a promise, which resolves to the expected
+ * asynchronous value.
+ * @param {msg} timeoutMsg
+ * The reason to reject the returned promise with.
+ * @param {number} [tryCount]
+ * Maximum times to try before rejecting the returned promise with
+ * timeoutMsg, defaults to NUMBER_OF_TRIES.
+ * @returns {Promise} to the asynchronous value being polled.
+ * @throws if the asynchronous value is not available after tryCount attempts.
+ */
+var waitForResolvedPromise = async function (
+ promiseFn,
+ timeoutMsg,
+ tryCount = NUMBER_OF_TRIES
+) {
+ let tries = 0;
+ do {
+ try {
+ let value = await promiseFn();
+ return value;
+ } catch (ex) {}
+ await new Promise(resolve => do_timeout(SINGLE_TRY_TIMEOUT, resolve));
+ } while (++tries <= tryCount);
+ throw new Error(timeoutMsg);
+};
diff --git a/browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js b/browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js
new file mode 100644
index 0000000000..58b68d7574
--- /dev/null
+++ b/browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js
@@ -0,0 +1,107 @@
+const { sinon } = ChromeUtils.importESModule(
+ "resource://testing-common/Sinon.sys.mjs"
+);
+
+/* eslint-disable mozilla/use-chromeutils-generateqi */
+
+add_task(async function test_no_result_node() {
+ let functionSpy = sinon.stub().returns(Promise.resolve());
+
+ await PlacesUIUtils.batchUpdatesForNode(null, 1, functionSpy);
+
+ Assert.ok(
+ functionSpy.calledOnce,
+ "Passing a null result node should still call the wrapped function"
+ );
+});
+
+add_task(async function test_under_batch_threshold() {
+ let functionSpy = sinon.stub().returns(Promise.resolve());
+ let resultNode = {
+ QueryInterface() {
+ return this;
+ },
+ onBeginUpdateBatch: sinon.spy(),
+ onEndUpdateBatch: sinon.spy(),
+ };
+
+ await PlacesUIUtils.batchUpdatesForNode(resultNode, 1, functionSpy);
+
+ Assert.ok(functionSpy.calledOnce, "Wrapped function should be called once");
+ Assert.ok(
+ resultNode.onBeginUpdateBatch.notCalled,
+ "onBeginUpdateBatch should not have been called"
+ );
+ Assert.ok(
+ resultNode.onEndUpdateBatch.notCalled,
+ "onEndUpdateBatch should not have been called"
+ );
+});
+
+add_task(async function test_over_batch_threshold() {
+ let functionSpy = sinon.stub().callsFake(() => {
+ Assert.ok(
+ resultNode.onBeginUpdateBatch.calledOnce,
+ "onBeginUpdateBatch should have been called before the function"
+ );
+ Assert.ok(
+ resultNode.onEndUpdateBatch.notCalled,
+ "onEndUpdateBatch should not have been called before the function"
+ );
+
+ return Promise.resolve();
+ });
+ let resultNode = {
+ QueryInterface() {
+ return this;
+ },
+ onBeginUpdateBatch: sinon.spy(),
+ onEndUpdateBatch: sinon.spy(),
+ };
+
+ await PlacesUIUtils.batchUpdatesForNode(resultNode, 100, functionSpy);
+
+ Assert.ok(functionSpy.calledOnce, "Wrapped function should be called once");
+ Assert.ok(
+ resultNode.onBeginUpdateBatch.calledOnce,
+ "onBeginUpdateBatch should have been called"
+ );
+ Assert.ok(
+ resultNode.onEndUpdateBatch.calledOnce,
+ "onEndUpdateBatch should have been called"
+ );
+});
+
+add_task(async function test_wrapped_function_throws() {
+ let error = new Error("Failed!");
+ let functionSpy = sinon.stub().throws(error);
+ let resultNode = {
+ QueryInterface() {
+ return this;
+ },
+ onBeginUpdateBatch: sinon.spy(),
+ onEndUpdateBatch: sinon.spy(),
+ };
+
+ let raisedError;
+ try {
+ await PlacesUIUtils.batchUpdatesForNode(resultNode, 100, functionSpy);
+ } catch (ex) {
+ raisedError = ex;
+ }
+
+ Assert.ok(functionSpy.calledOnce, "Wrapped function should be called once");
+ Assert.ok(
+ resultNode.onBeginUpdateBatch.calledOnce,
+ "onBeginUpdateBatch should have been called"
+ );
+ Assert.ok(
+ resultNode.onEndUpdateBatch.calledOnce,
+ "onEndUpdateBatch should have been called"
+ );
+ Assert.equal(
+ raisedError,
+ error,
+ "batchUpdatesForNode should have raised the error from the wrapped function"
+ );
+});
diff --git a/browser/components/places/tests/unit/test_PUIU_setCharsetForPage.js b/browser/components/places/tests/unit/test_PUIU_setCharsetForPage.js
new file mode 100644
index 0000000000..db213971e9
--- /dev/null
+++ b/browser/components/places/tests/unit/test_PUIU_setCharsetForPage.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 { PrivateBrowsingUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/PrivateBrowsingUtils.sys.mjs"
+);
+
+const UTF8 = "UTF-8";
+const UTF16 = "UTF-16";
+
+const TEST_URI = "http://foo.com";
+const TEST_BOOKMARKED_URI = "http://bar.com";
+
+add_task(function setup() {
+ let savedIsWindowPrivateFunc = PrivateBrowsingUtils.isWindowPrivate;
+ PrivateBrowsingUtils.isWindowPrivate = () => false;
+
+ registerCleanupFunction(() => {
+ PrivateBrowsingUtils.isWindowPrivate = savedIsWindowPrivateFunc;
+ });
+});
+
+add_task(async function test_simple_add() {
+ // add pages to history
+ await PlacesTestUtils.addVisits(TEST_URI);
+ await PlacesTestUtils.addVisits(TEST_BOOKMARKED_URI);
+
+ // create bookmarks on TEST_BOOKMARKED_URI
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.unfiledGuid,
+ url: TEST_BOOKMARKED_URI,
+ title: TEST_BOOKMARKED_URI.spec,
+ });
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ url: TEST_BOOKMARKED_URI,
+ title: TEST_BOOKMARKED_URI.spec,
+ });
+
+ // set charset on not-bookmarked page
+ await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF16, {});
+ // set charset on bookmarked page
+ await PlacesUIUtils.setCharsetForPage(TEST_BOOKMARKED_URI, UTF16, {});
+
+ let pageInfo = await PlacesUtils.history.fetch(TEST_URI, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ UTF16,
+ "Should return correct charset for a not-bookmarked page"
+ );
+
+ pageInfo = await PlacesUtils.history.fetch(TEST_BOOKMARKED_URI, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ UTF16,
+ "Should return correct charset for a bookmarked page"
+ );
+
+ await PlacesUtils.history.clear();
+
+ pageInfo = await PlacesUtils.history.fetch(TEST_URI, {
+ includeAnnotations: true,
+ });
+ Assert.ok(
+ !pageInfo,
+ "Should not return pageInfo for a page after history cleared"
+ );
+
+ pageInfo = await PlacesUtils.history.fetch(TEST_BOOKMARKED_URI, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ UTF16,
+ "Charset should still be set for a bookmarked page after history clear"
+ );
+
+ await PlacesUIUtils.setCharsetForPage(TEST_BOOKMARKED_URI, "");
+ pageInfo = await PlacesUtils.history.fetch(TEST_BOOKMARKED_URI, {
+ includeAnnotations: true,
+ });
+ Assert.strictEqual(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ undefined,
+ "Should not have a charset after it has been removed from the page"
+ );
+});
+
+add_task(async function test_utf8_clears_saved_anno() {
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(TEST_URI);
+
+ // set charset on bookmarked page
+ await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF16, {});
+
+ let pageInfo = await PlacesUtils.history.fetch(TEST_URI, {
+ includeAnnotations: true,
+ });
+ Assert.equal(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ UTF16,
+ "Should return correct charset for a not-bookmarked page"
+ );
+
+ // Now set the bookmark to a UTF-8 charset.
+ await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF8, {});
+
+ pageInfo = await PlacesUtils.history.fetch(TEST_URI, {
+ includeAnnotations: true,
+ });
+ Assert.strictEqual(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ undefined,
+ "Should have removed the charset for a UTF-8 page."
+ );
+});
+
+add_task(async function test_private_browsing_not_saved() {
+ await PlacesUtils.history.clear();
+ await PlacesTestUtils.addVisits(TEST_URI);
+
+ // set charset on bookmarked page, but pretend this is a private browsing window.
+ PrivateBrowsingUtils.isWindowPrivate = () => true;
+ await PlacesUIUtils.setCharsetForPage(TEST_URI, UTF16, {});
+
+ let pageInfo = await PlacesUtils.history.fetch(TEST_URI, {
+ includeAnnotations: true,
+ });
+ Assert.strictEqual(
+ pageInfo.annotations.get(PlacesUtils.CHARSET_ANNO),
+ undefined,
+ "Should not have set the charset in a private browsing window."
+ );
+});
diff --git a/browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js b/browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js
new file mode 100644
index 0000000000..aaa4db6bb2
--- /dev/null
+++ b/browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js
@@ -0,0 +1,81 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the difference indication for titles.
+ */
+
+const TESTS = [
+ {
+ title: null,
+ expected: undefined,
+ },
+ {
+ title: "Short title",
+ expected: undefined,
+ },
+ {
+ title: "Short title2",
+ expected: undefined,
+ },
+ {
+ title: "Long title same as another",
+ expected: undefined,
+ },
+ {
+ title: "Long title same as another",
+ expected: undefined,
+ },
+ {
+ title: "Long title with difference at the end 1",
+ expected: 38,
+ },
+ {
+ title: "Long title with difference at the end 2",
+ expected: 38,
+ },
+ {
+ title: "A long title with difference 123456 in the middle.",
+ expected: 30,
+ },
+ {
+ title: "A long title with difference 135246 in the middle.",
+ expected: 30,
+ },
+ {
+ title:
+ "Some long titles with variable 12345678 differences to 13572468 other titles",
+ expected: 32,
+ },
+ {
+ title:
+ "Some long titles with variable 12345678 differences to 15263748 other titles",
+ expected: 32,
+ },
+ {
+ title:
+ "Some long titles with variable 15263748 differences to 12345678 other titles",
+ expected: 32,
+ },
+ {
+ title: "One long title which will be shorter than the other one",
+ expected: 40,
+ },
+ {
+ title:
+ "One long title which will be shorter that the other one (not this one)",
+ expected: 40,
+ },
+];
+
+add_task(async function test_difference_finding() {
+ PlacesUIUtils.insertTitleStartDiffs(TESTS);
+
+ for (let result of TESTS) {
+ Assert.equal(
+ result.titleDifferentIndex,
+ result.expected,
+ `Should have returned the correct index for "${result.title}"`
+ );
+ }
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
new file mode 100644
index 0000000000..65a038487a
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js
@@ -0,0 +1,33 @@
+/* -*- 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 that nsBrowserGlue correctly exports bookmarks.html at shutdown if
+ * browser.bookmarks.autoExportHTML is set to true.
+ */
+
+add_task(async function () {
+ remove_bookmarks_html();
+
+ Services.prefs.setBoolPref("browser.bookmarks.autoExportHTML", true);
+ registerCleanupFunction(() =>
+ Services.prefs.clearUserPref("browser.bookmarks.autoExportHTML")
+ );
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Initialize Places through the History Service.
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE
+ );
+
+ Services.obs.addObserver(function observer() {
+ Services.obs.removeObserver(observer, "profile-before-change");
+ check_bookmarks_html();
+ }, "profile-before-change");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt.js b/browser/components/places/tests/unit/test_browserGlue_corrupt.js
new file mode 100644
index 0000000000..0514b06bbc
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt.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/. */
+
+/**
+ * Tests that nsBrowserGlue correctly restores bookmarks from a JSON backup if
+ * database is corrupt and one backup is available.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ remove_all_JSON_backups();
+
+ // Create our JSON backup from bookmarks.glue.json.
+ create_JSON_backup("bookmarks.glue.json");
+
+ run_next_test();
+}
+
+registerCleanupFunction(function () {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+ return PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_main() {
+ // Create a corrupt database.
+ await createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT
+ );
+
+ // The test will continue once restore has finished.
+ await promiseTopicObserved("places-browser-init-complete");
+
+ // Check that JSON backup has been restored.
+ let bm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+ Assert.equal(bm.title, "examplejson");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
new file mode 100644
index 0000000000..a0bc93c74c
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js
@@ -0,0 +1,47 @@
+/* -*- 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 that nsBrowserGlue correctly imports from bookmarks.html if database
+ * is corrupt but a JSON backup is not available.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ run_next_test();
+}
+
+registerCleanupFunction(remove_bookmarks_html);
+
+add_task(async function () {
+ // Create a corrupt database.
+ await createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT
+ );
+
+ // The test will continue once import has finished.
+ await promiseTopicObserved("places-browser-init-complete");
+
+ // Check that bookmarks html has been restored.
+ let bm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+ Assert.equal(bm.title, "example");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js
new file mode 100644
index 0000000000..031d82d9e2
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.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/. */
+
+/**
+ * Tests that nsBrowserGlue correctly restores default bookmarks if database is
+ * corrupt, nor a JSON backup nor bookmarks.html are available.
+ */
+
+function run_test() {
+ // Remove bookmarks.html from profile.
+ remove_bookmarks_html();
+
+ // Remove JSON backup from profile.
+ remove_all_JSON_backups();
+
+ run_next_test();
+}
+
+add_task(async function () {
+ // Create a corrupt database.
+ await createCorruptDB();
+
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Check the database was corrupt.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CORRUPT
+ );
+
+ // The test will continue once import has finished.
+ await promiseTopicObserved("places-browser-init-complete");
+
+ // Check that default bookmarks have been restored.
+ let bm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+
+ // Bug 1283076: Nightly bookmark points to Get Involved page, not Getting Started one
+ let chanTitle = AppConstants.NIGHTLY_BUILD
+ ? "Get Involved"
+ : "Getting Started";
+ Assert.equal(bm.title, chanTitle);
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_distribution.js b/browser/components/places/tests/unit/test_browserGlue_distribution.js
new file mode 100644
index 0000000000..5fafba457f
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_distribution.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue correctly imports bookmarks from distribution.ini.
+ */
+
+const PREF_BMPROCESSED = "distribution.516444.bookmarksProcessed";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+
+const TOPICDATA_DISTRIBUTION_CUSTOMIZATION = "force-distribution-customization";
+const TOPIC_CUSTOMIZATION_COMPLETE = "distribution-customization-complete";
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+
+function run_test() {
+ // Set special pref to load distribution.ini from the profile folder.
+ Services.prefs.setBoolPref("distribution.testing.loadFromProfile", true);
+
+ // Copy distribution.ini file to the profile dir.
+ let distroDir = gProfD.clone();
+ distroDir.leafName = "distribution";
+ let iniFile = distroDir.clone();
+ iniFile.append("distribution.ini");
+ if (iniFile.exists()) {
+ iniFile.remove(false);
+ print("distribution.ini already exists, did some test forget to cleanup?");
+ }
+
+ let testDistributionFile = gTestDir.clone();
+ testDistributionFile.append("distribution.ini");
+ testDistributionFile.copyTo(distroDir, "distribution.ini");
+ Assert.ok(testDistributionFile.exists());
+
+ run_next_test();
+}
+
+registerCleanupFunction(function () {
+ // Remove the distribution file, even if the test failed, otherwise all
+ // next tests will import it.
+ let iniFile = gProfD.clone();
+ iniFile.leafName = "distribution";
+ iniFile.append("distribution.ini");
+ if (iniFile.exists()) {
+ iniFile.remove(false);
+ }
+ Assert.ok(!iniFile.exists());
+});
+
+add_task(async function () {
+ let { DistributionCustomizer } = ChromeUtils.importESModule(
+ "resource:///modules/distribution.sys.mjs"
+ );
+ let distribution = new DistributionCustomizer();
+
+ let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+ );
+ // Initialize Places through the History Service and check that a new
+ // database has been created.
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE
+ );
+ // Force distribution.
+ glue.observe(
+ null,
+ TOPIC_BROWSERGLUE_TEST,
+ TOPICDATA_DISTRIBUTION_CUSTOMIZATION
+ );
+
+ // Test will continue on customization complete notification.
+ await promiseTopicObserved(TOPIC_CUSTOMIZATION_COMPLETE);
+
+ // Check the custom bookmarks exist on menu.
+ let menuItem = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ });
+ Assert.equal(menuItem.title, "Menu Link Before");
+ Assert.ok(
+ menuItem.guid.startsWith(distribution.BOOKMARK_GUID_PREFIX),
+ "Guid of this bookmark has expected prefix"
+ );
+
+ menuItem = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 1 + DEFAULT_BOOKMARKS_ON_MENU,
+ });
+ Assert.equal(menuItem.title, "Menu Link After");
+
+ // Check no favicon exists for this bookmark
+ await Assert.rejects(
+ waitForResolvedPromise(
+ () => {
+ return PlacesUtils.promiseFaviconData(menuItem.url.href);
+ },
+ "Favicon not found",
+ 10
+ ),
+ /Favicon\snot\sfound/,
+ "Favicon not found"
+ );
+
+ // Check the custom bookmarks exist on toolbar.
+ let toolbarItem = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+ Assert.equal(toolbarItem.title, "Toolbar Link Before");
+
+ // Check the custom favicon exist for this bookmark
+ let faviconItem = await waitForResolvedPromise(
+ () => {
+ return PlacesUtils.promiseFaviconData(toolbarItem.url.href);
+ },
+ "Favicon not found",
+ 10
+ );
+ Assert.equal(faviconItem.uri.spec, "https://example.org/favicon.png");
+ Assert.greater(faviconItem.dataLen, 0);
+ Assert.equal(faviconItem.mimeType, "image/png");
+
+ let base64Icon =
+ "data:image/png;base64," +
+ base64EncodeString(String.fromCharCode.apply(String, faviconItem.data));
+ Assert.equal(base64Icon, SMALLPNG_DATA_URI.spec);
+
+ toolbarItem = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 1 + DEFAULT_BOOKMARKS_ON_TOOLBAR,
+ });
+ Assert.equal(toolbarItem.title, "Toolbar Folder After");
+ Assert.ok(
+ toolbarItem.guid.startsWith(distribution.FOLDER_GUID_PREFIX),
+ "Guid of this folder has expected prefix"
+ );
+
+ // Check the bmprocessed pref has been created.
+ Assert.ok(Services.prefs.getBoolPref(PREF_BMPROCESSED));
+
+ // Check distribution prefs have been created.
+ Assert.equal(Services.prefs.getCharPref(PREF_DISTRIBUTION_ID), "516444");
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_migrate.js b/browser/components/places/tests/unit/test_browserGlue_migrate.js
new file mode 100644
index 0000000000..f0f88d2d17
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_migrate.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue does not overwrite bookmarks imported from the
+ * migrators. They usually run before nsBrowserGlue, so if we find any
+ * bookmark on init, we should not try to import.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ // Remove current database file.
+ clearDB();
+
+ run_next_test();
+}
+
+registerCleanupFunction(remove_bookmarks_html);
+
+add_task(async function test_migrate_bookmarks() {
+ // Initialize Places through the History Service and check that a new
+ // database has been created.
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE
+ );
+
+ // A migrator would run before nsBrowserGlue Places initialization, so mimic
+ // that behavior adding a bookmark and notifying the migration.
+ let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
+ bg.observe(null, "initial-migration-will-import-default-bookmarks", null);
+
+ await PlacesUtils.bookmarks.insert({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: PlacesUtils.bookmarks.DEFAULT_INDEX,
+ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
+ url: "http://mozilla.org/",
+ title: "migrated",
+ });
+
+ let promise = promiseTopicObserved("places-browser-init-complete");
+ bg.observe(null, "initial-migration-did-import-default-bookmarks", null);
+ await promise;
+
+ // Check the created bookmark still exists.
+ let bm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 0,
+ });
+ Assert.equal(bm.title, "migrated");
+
+ // Check that we have not imported any new bookmark.
+ Assert.ok(
+ !(await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.menuGuid,
+ index: 1,
+ }))
+ );
+
+ Assert.ok(
+ !(await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ }))
+ );
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_prefs.js b/browser/components/places/tests/unit/test_browserGlue_prefs.js
new file mode 100644
index 0000000000..af1dc3db0e
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_prefs.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that nsBrowserGlue is correctly interpreting the preferences settable
+ * by the user or by other components.
+ */
+
+const PREF_IMPORT_BOOKMARKS_HTML = "browser.places.importBookmarksHTML";
+const PREF_RESTORE_DEFAULT_BOOKMARKS =
+ "browser.bookmarks.restore_default_bookmarks";
+const PREF_AUTO_EXPORT_HTML = "browser.bookmarks.autoExportHTML";
+
+const TOPIC_BROWSERGLUE_TEST = "browser-glue-test";
+const TOPICDATA_FORCE_PLACES_INIT = "test-force-places-init";
+
+var bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver);
+
+add_task(async function setup() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ remove_all_JSON_backups();
+
+ // Create our JSON backup from bookmarks.glue.json.
+ create_JSON_backup("bookmarks.glue.json");
+
+ registerCleanupFunction(function () {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+
+ return PlacesUtils.bookmarks.eraseEverything();
+ });
+});
+
+function simulatePlacesInit() {
+ info("Simulate Places init");
+ // Force nsBrowserGlue::_initPlaces().
+ bg.observe(null, TOPIC_BROWSERGLUE_TEST, TOPICDATA_FORCE_PLACES_INIT);
+ return promiseTopicObserved("places-browser-init-complete");
+}
+
+add_task(async function test_checkPreferences() {
+ // Initialize Places through the History Service and check that a new
+ // database has been created.
+ let promiseComplete = promiseTopicObserved("places-browser-init-complete");
+ Assert.equal(
+ PlacesUtils.history.databaseStatus,
+ PlacesUtils.history.DATABASE_STATUS_CREATE
+ );
+ await promiseComplete;
+
+ // Ensure preferences status.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_AUTO_EXPORT_HTML));
+
+ Assert.throws(
+ () => Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML),
+ /NS_ERROR_UNEXPECTED/
+ );
+ Assert.throws(
+ () => Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS),
+ /NS_ERROR_UNEXPECTED/
+ );
+});
+
+add_task(async function test_import() {
+ info("Import from bookmarks.html if importBookmarksHTML is true.");
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(
+ !(await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ }))
+ );
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+
+ await simulatePlacesInit();
+
+ // Check bookmarks.html has been imported.
+ let bm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+ Assert.equal(bm.title, "example");
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
+
+add_task(async function test_restore() {
+ info(
+ "restore from default bookmarks.html if " +
+ "restore_default_bookmarks is true."
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(
+ !(await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ }))
+ );
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+ await simulatePlacesInit();
+
+ // Check bookmarks.html has been restored.
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ })
+ );
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+});
+
+add_task(async function test_restore_import() {
+ info(
+ "setting both importBookmarksHTML and " +
+ "restore_default_bookmarks should restore defaults."
+ );
+
+ await PlacesUtils.bookmarks.eraseEverything();
+
+ // Sanity check: we should not have any bookmark on the toolbar.
+ Assert.ok(
+ !(await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ }))
+ );
+
+ // Set preferences.
+ Services.prefs.setBoolPref(PREF_IMPORT_BOOKMARKS_HTML, true);
+ Services.prefs.setBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS, true);
+
+ await simulatePlacesInit();
+
+ // Check bookmarks.html has been restored.
+ Assert.ok(
+ await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ })
+ );
+
+ // Check preferences have been reverted.
+ Assert.ok(!Services.prefs.getBoolPref(PREF_RESTORE_DEFAULT_BOOKMARKS));
+ Assert.ok(!Services.prefs.getBoolPref(PREF_IMPORT_BOOKMARKS_HTML));
+});
diff --git a/browser/components/places/tests/unit/test_browserGlue_restore.js b/browser/components/places/tests/unit/test_browserGlue_restore.js
new file mode 100644
index 0000000000..98c8d1d2d6
--- /dev/null
+++ b/browser/components/places/tests/unit/test_browserGlue_restore.js
@@ -0,0 +1,55 @@
+/* -*- 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 that nsBrowserGlue correctly restores bookmarks from a JSON backup if
+ * database has been created and one backup is available.
+ */
+
+function run_test() {
+ // Create our bookmarks.html from bookmarks.glue.html.
+ create_bookmarks_html("bookmarks.glue.html");
+
+ remove_all_JSON_backups();
+
+ // Create our JSON backup from bookmarks.glue.json.
+ create_JSON_backup("bookmarks.glue.json");
+
+ // Remove current database file.
+ clearDB();
+
+ run_next_test();
+}
+
+registerCleanupFunction(function () {
+ remove_bookmarks_html();
+ remove_all_JSON_backups();
+ return PlacesUtils.bookmarks.eraseEverything();
+});
+
+add_task(async function test_main() {
+ // Initialize nsBrowserGlue before Places.
+ Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsISupports);
+
+ // Initialize Places through the History Service.
+ let hs = Cc["@mozilla.org/browser/nav-history-service;1"].getService(
+ Ci.nsINavHistoryService
+ );
+
+ // Check a new database has been created.
+ // nsBrowserGlue uses databaseStatus to manage initialization.
+ Assert.equal(hs.databaseStatus, hs.DATABASE_STATUS_CREATE);
+
+ // The test will continue once restore has finished.
+ await promiseTopicObserved("places-browser-init-complete");
+
+ // Check that JSON backup has been restored.
+ let bm = await PlacesUtils.bookmarks.fetch({
+ parentGuid: PlacesUtils.bookmarks.toolbarGuid,
+ index: 0,
+ });
+ Assert.equal(bm.title, "examplejson");
+});
diff --git a/browser/components/places/tests/unit/test_clearHistory_shutdown.js b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
new file mode 100644
index 0000000000..27b432e569
--- /dev/null
+++ b/browser/components/places/tests/unit/test_clearHistory_shutdown.js
@@ -0,0 +1,183 @@
+/* -*- 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 that requesting clear history at shutdown will really clear history.
+ */
+
+const URIS = [
+ "http://a.example1.com/",
+ "http://b.example1.com/",
+ "http://b.example2.com/",
+ "http://c.example3.com/",
+];
+
+const FTP_URL = "ftp://localhost/clearHistoryOnShutdown/";
+
+const { Sanitizer } = ChromeUtils.importESModule(
+ "resource:///modules/Sanitizer.sys.mjs"
+);
+
+// Send the profile-after-change notification to the form history component to ensure
+// that it has been initialized.
+var formHistoryStartup = Cc[
+ "@mozilla.org/satchel/form-history-startup;1"
+].getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+ChromeUtils.defineESModuleGetters(this, {
+ FormHistory: "resource://gre/modules/FormHistory.sys.mjs",
+});
+
+var timeInMicroseconds = Date.now() * 1000;
+
+add_task(async function test_execute() {
+ info("Initialize browserglue before Places");
+
+ // Avoid default bookmarks import.
+ let glue = Cc["@mozilla.org/browser/browserglue;1"].getService(
+ Ci.nsIObserver
+ );
+ glue.observe(null, "initial-migration-will-import-default-bookmarks", null);
+ Sanitizer.onStartup();
+
+ Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cache", true);
+ Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true);
+ Services.prefs.setBoolPref(
+ Sanitizer.PREF_SHUTDOWN_BRANCH + "offlineApps",
+ true
+ );
+ Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "history", true);
+ Services.prefs.setBoolPref(
+ Sanitizer.PREF_SHUTDOWN_BRANCH + "downloads",
+ true
+ );
+ Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "cookies", true);
+ Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "formData", true);
+ Services.prefs.setBoolPref(Sanitizer.PREF_SHUTDOWN_BRANCH + "sessions", true);
+ Services.prefs.setBoolPref(
+ Sanitizer.PREF_SHUTDOWN_BRANCH + "siteSettings",
+ true
+ );
+
+ Services.prefs.setBoolPref(Sanitizer.PREF_SANITIZE_ON_SHUTDOWN, true);
+
+ info("Add visits.");
+ for (let aUrl of URIS) {
+ await PlacesTestUtils.addVisits({
+ uri: uri(aUrl),
+ visitDate: timeInMicroseconds++,
+ transition: PlacesUtils.history.TRANSITION_TYPED,
+ });
+ }
+ info("Add cache.");
+ await storeCache(FTP_URL, "testData");
+ info("Add form history.");
+ await addFormHistory();
+ Assert.equal(await getFormHistoryCount(), 1, "Added form history");
+
+ info("Simulate and wait shutdown.");
+ await shutdownPlaces();
+
+ Assert.equal(await getFormHistoryCount(), 0, "Form history cleared");
+
+ let stmt = DBConn(true).createStatement(
+ "SELECT id FROM moz_places WHERE url = :page_url "
+ );
+
+ try {
+ URIS.forEach(function (aUrl) {
+ stmt.params.page_url = aUrl;
+ Assert.ok(!stmt.executeStep());
+ stmt.reset();
+ });
+ } finally {
+ stmt.finalize();
+ }
+
+ info("Check cache");
+ // Check cache.
+ await checkCache(FTP_URL);
+});
+
+function addFormHistory() {
+ let now = Date.now() * 1000;
+ return FormHistory.update({
+ op: "add",
+ fieldname: "testfield",
+ value: "test",
+ timesUsed: 1,
+ firstUsed: now,
+ lastUsed: now,
+ });
+}
+
+async function getFormHistoryCount() {
+ return FormHistory.count({ fieldname: "testfield" });
+}
+
+function storeCache(aURL, aContent) {
+ let cache = Services.cache2;
+ let storage = cache.diskCacheStorage(Services.loadContextInfo.default);
+
+ return new Promise(resolve => {
+ let storeCacheListener = {
+ onCacheEntryCheck(entry) {
+ return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED;
+ },
+
+ onCacheEntryAvailable(entry, isnew, status) {
+ Assert.equal(status, Cr.NS_OK);
+
+ entry.setMetaDataElement("servertype", "0");
+ var os = entry.openOutputStream(0, -1);
+
+ var written = os.write(aContent, aContent.length);
+ if (written != aContent.length) {
+ do_throw(
+ "os.write has not written all data!\n" +
+ " Expected: " +
+ written +
+ "\n" +
+ " Actual: " +
+ aContent.length +
+ "\n"
+ );
+ }
+ os.close();
+ entry.close();
+ resolve();
+ },
+ };
+
+ storage.asyncOpenURI(
+ Services.io.newURI(aURL),
+ "",
+ Ci.nsICacheStorage.OPEN_NORMALLY,
+ storeCacheListener
+ );
+ });
+}
+
+function checkCache(aURL) {
+ let cache = Services.cache2;
+ let storage = cache.diskCacheStorage(Services.loadContextInfo.default);
+
+ return new Promise(resolve => {
+ let checkCacheListener = {
+ onCacheEntryAvailable(entry, isnew, status) {
+ Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND);
+ resolve();
+ },
+ };
+
+ storage.asyncOpenURI(
+ Services.io.newURI(aURL),
+ "",
+ Ci.nsICacheStorage.OPEN_READONLY,
+ checkCacheListener
+ );
+ });
+}
diff --git a/browser/components/places/tests/unit/test_interactions_blocklist.js b/browser/components/places/tests/unit/test_interactions_blocklist.js
new file mode 100644
index 0000000000..0e81f80af2
--- /dev/null
+++ b/browser/components/places/tests/unit/test_interactions_blocklist.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests that blocked sites are caught by InteractionsBlocklist.
+ */
+
+ChromeUtils.defineESModuleGetters(this, {
+ InteractionsBlocklist: "resource:///modules/InteractionsBlocklist.sys.mjs",
+});
+
+let BLOCKED_URLS = [
+ "https://www.bing.com/search?q=mozilla",
+ "https://duckduckgo.com/?q=a+test&kp=1&t=ffab",
+ "https://www.google.com/search?q=mozilla",
+ "https://www.google.ca/search?q=test",
+ "https://mozilla.zoom.us/j/123456789",
+ "https://yandex.az/search/?text=mozilla",
+ "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=mozilla&rn=&fenlei=256&oq=&rsv_pq=970f2b8f001757b9&rsv_t=1f5d2V2o80HPdZtZnhodwkc7nZXTvDI1zwdPy%2FAeomnvFFGIrU1F3D9WoK4&rqlang=cn",
+ "https://accounts.google.com/o/oauth2/v2/auth/identifier/foobar",
+ "https://auth.mozilla.auth0.com/login/foobar",
+ "https://accounts.google.com/signin/oauth/consent/foobar",
+ "https://accounts.google.com/o/oauth2/v2/auth?client_id=ZZZ",
+ "https://login.microsoftonline.com/common/oauth2/v2.0/authorize/foobar",
+];
+
+let ALLOWED_URLS = [
+ "https://example.com",
+ "https://zoom.us/pricing",
+ "https://www.google.ca/maps/place/Toronto,+ON/@43.7181557,-79.5181414,11z/data=!3m1!4b1!4m5!3m4!1s0x89d4cb90d7c63ba5:0x323555502ab4c477!8m2!3d43.653226!4d-79.3831843",
+ "https://example.com/https://auth.mozilla.auth0.com/login/foobar",
+];
+
+// Tests that initializing InteractionsBlocklist loads the regexes from the
+// customBlocklist pref on initialization. This subtest should always be the
+// first one in this file.
+add_task(async function blockedOnInit() {
+ Services.prefs.setStringPref(
+ "places.interactions.customBlocklist",
+ '["^(https?:\\\\/\\\\/)?mochi.test"]'
+ );
+ Assert.ok(
+ InteractionsBlocklist.isUrlBlocklisted("https://mochi.test"),
+ "mochi.test is blocklisted."
+ );
+ InteractionsBlocklist.removeRegexFromBlocklist("^(https?:\\/\\/)?mochi.test");
+ Assert.ok(
+ !InteractionsBlocklist.isUrlBlocklisted("https://mochi.test"),
+ "mochi.test is not blocklisted."
+ );
+});
+
+add_task(async function test() {
+ for (let url of BLOCKED_URLS) {
+ Assert.ok(
+ InteractionsBlocklist.isUrlBlocklisted(url),
+ `${url} is blocklisted.`
+ );
+ }
+
+ for (let url of ALLOWED_URLS) {
+ Assert.ok(
+ !InteractionsBlocklist.isUrlBlocklisted(url),
+ `${url} is not blocklisted.`
+ );
+ }
+});
diff --git a/browser/components/places/tests/unit/test_invalid_defaultLocation.js b/browser/components/places/tests/unit/test_invalid_defaultLocation.js
new file mode 100644
index 0000000000..e533d051d0
--- /dev/null
+++ b/browser/components/places/tests/unit/test_invalid_defaultLocation.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ * https://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+/**
+ * Test that if browser.bookmarks.defaultLocation contains an invalid GUID,
+ * PlacesUIUtils.defaultParentGuid will return a proper default value.
+ */
+
+add_task(async function () {
+ Services.prefs.setCharPref(
+ "browser.bookmarks.defaultLocation",
+ "useOtherBookmarks"
+ );
+
+ info(
+ "Checking that default parent guid was set back to the toolbar because of invalid preferable guid"
+ );
+ Assert.equal(
+ await PlacesUIUtils.defaultParentGuid,
+ PlacesUtils.bookmarks.toolbarGuid,
+ "Default parent guid is a toolbar guid"
+ );
+});
diff --git a/browser/components/places/tests/unit/xpcshell.toml b/browser/components/places/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..75727e359a
--- /dev/null
+++ b/browser/components/places/tests/unit/xpcshell.toml
@@ -0,0 +1,38 @@
+[DEFAULT]
+head = "head_bookmarks.js"
+firefox-appdir = "browser"
+skip-if = ["os == 'android'"] # bug 1730213
+support-files = [
+ "bookmarks.glue.html",
+ "bookmarks.glue.json",
+ "corruptDB.sqlite",
+ "distribution.ini",
+]
+
+["test_PUIU_batchUpdatesForNode.js"]
+
+["test_PUIU_setCharsetForPage.js"]
+
+["test_PUIU_title_difference_spotter.js"]
+
+["test_browserGlue_bookmarkshtml.js"]
+
+["test_browserGlue_corrupt.js"]
+
+["test_browserGlue_corrupt_nobackup.js"]
+
+["test_browserGlue_corrupt_nobackup_default.js"]
+
+["test_browserGlue_distribution.js"]
+
+["test_browserGlue_migrate.js"]
+
+["test_browserGlue_prefs.js"]
+
+["test_browserGlue_restore.js"]
+
+["test_clearHistory_shutdown.js"]
+
+["test_interactions_blocklist.js"]
+
+["test_invalid_defaultLocation.js"]