From 36d22d82aa202bb199967e9512281e9a53db42c9 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 21:33:14 +0200 Subject: Adding upstream version 115.7.0esr. Signed-off-by: Daniel Baumann --- .../places/tests/browser/bookmark_dummy_1.html | 9 + .../places/tests/browser/bookmark_dummy_2.html | 9 + .../browser/bookmarklet_windowOpen_dummy.html | 9 + .../components/places/tests/browser/browser.ini | 132 ++++ .../tests/browser/browser_addBookmarkForFrame.js | 150 ++++ .../browser/browser_autoshow_bookmarks_toolbar.js | 165 +++++ .../browser/browser_bookmarkMenu_hiddenWindow.js | 49 ++ ...er_bookmarkProperties_addFolderDefaultButton.js | 68 ++ ...r_bookmarkProperties_addKeywordForThisSearch.js | 188 +++++ .../browser_bookmarkProperties_bookmarkAllTabs.js | 66 ++ .../browser/browser_bookmarkProperties_cancel.js | 126 ++++ .../browser_bookmarkProperties_editFolder.js | 78 ++ .../browser_bookmarkProperties_editTagContainer.js | 140 ++++ .../browser_bookmarkProperties_folderSelection.js | 170 +++++ .../browser_bookmarkProperties_newFolder.js | 110 +++ .../browser_bookmarkProperties_no_user_actions.js | 89 +++ .../browser_bookmarkProperties_readOnlyRoot.js | 67 ++ .../browser_bookmarkProperties_remember_folders.js | 221 ++++++ ...ser_bookmarkProperties_speculativeConnection.js | 106 +++ .../browser/browser_bookmarkProperties_xulStore.js | 47 ++ .../tests/browser/browser_bookmark_add_tags.js | 220 ++++++ .../tests/browser/browser_bookmark_all_tabs.js | 46 ++ .../browser_bookmark_backup_export_import.js | 205 ++++++ .../browser/browser_bookmark_change_location.js | 213 ++++++ .../browser_bookmark_context_menu_contents.js | 798 +++++++++++++++++++++ .../browser/browser_bookmark_copy_folder_tree.js | 172 +++++ .../browser/browser_bookmark_folder_moveability.js | 139 ++++ .../browser/browser_bookmark_menu_ctrl_click.js | 43 ++ .../places/tests/browser/browser_bookmark_popup.js | 712 ++++++++++++++++++ .../browser/browser_bookmark_private_window.js | 58 ++ .../tests/browser/browser_bookmark_remove_tags.js | 256 +++++++ .../tests/browser/browser_bookmark_titles.js | 129 ++++ .../browser/browser_bookmarklet_windowOpen.js | 79 ++ .../tests/browser/browser_bookmarksProperties.js | 526 ++++++++++++++ .../browser/browser_bookmarks_change_title.js | 256 +++++++ .../tests/browser/browser_bookmarks_change_url.js | 106 +++ .../browser/browser_bookmarks_sidebar_search.js | 213 ++++++ ..._bookmarks_toolbar_context_menu_view_options.js | 87 +++ .../browser/browser_bookmarks_toolbar_telemetry.js | 162 +++++ .../browser_bug427633_no_newfolder_if_noip.js | 50 ++ .../browser_bug485100-change-case-loses-tag.js | 74 ++ .../browser_bug631374_tags_selector_scroll.js | 162 +++++ .../browser/browser_check_correct_controllers.js | 110 +++ .../browser/browser_click_bookmarks_on_toolbar.js | 234 ++++++ .../tests/browser/browser_controller_onDrop.js | 125 ++++ .../browser/browser_controller_onDrop_query.js | 132 ++++ .../browser/browser_controller_onDrop_sidebar.js | 312 ++++++++ .../browser/browser_controller_onDrop_tagFolder.js | 125 ++++ .../browser/browser_copy_query_without_tree.js | 113 +++ .../tests/browser/browser_cutting_bookmarks.js | 87 +++ .../browser/browser_default_bookmark_location.js | 274 +++++++ .../browser/browser_drag_bookmarks_on_toolbar.js | 255 +++++++ .../tests/browser/browser_drag_folder_on_newTab.js | 100 +++ .../tests/browser/browser_editBookmark_keywords.js | 64 ++ .../browser/browser_enable_toolbar_sidebar.js | 57 ++ .../places/tests/browser/browser_forgetthissite.js | 262 +++++++ .../browser/browser_history_sidebar_search.js | 71 ++ .../places/tests/browser/browser_import_button.js | 187 +++++ .../browser_library_bookmark_clear_visits.js | 147 ++++ .../browser/browser_library_bookmark_pages.js | 101 +++ .../browser/browser_library_bulk_tag_bookmarks.js | 146 ++++ .../tests/browser/browser_library_commands.js | 335 +++++++++ .../places/tests/browser/browser_library_delete.js | 116 +++ .../browser_library_delete_bookmarks_in_tags.js | 111 +++ .../tests/browser/browser_library_delete_tags.js | 60 ++ .../tests/browser/browser_library_downloads.js | 65 ++ .../browser_library_left_pane_middleclick.js | 106 +++ .../browser_library_left_pane_select_hierarchy.js | 52 ++ .../tests/browser/browser_library_middleclick.js | 234 ++++++ .../tests/browser/browser_library_new_bookmark.js | 95 +++ .../browser/browser_library_openFlatContainer.js | 125 ++++ .../tests/browser/browser_library_open_all.js | 57 ++ .../browser_library_open_all_with_separator.js | 61 ++ .../tests/browser/browser_library_open_bookmark.js | 46 ++ .../tests/browser/browser_library_open_leak.js | 22 + .../tests/browser/browser_library_panel_leak.js | 71 ++ .../places/tests/browser/browser_library_search.js | 206 ++++++ .../tests/browser/browser_library_telemetry.js | 413 +++++++++++ .../tests/browser/browser_library_tree_leak.js | 24 + .../browser/browser_library_views_liveupdate.js | 255 +++++++ .../tests/browser/browser_library_warnOnOpen.js | 159 ++++ .../browser/browser_markPageAsFollowedLink.js | 75 ++ .../browser/browser_panelview_bookmarks_delete.js | 58 ++ .../tests/browser/browser_paste_bookmarks.js | 418 +++++++++++ .../tests/browser/browser_paste_into_tags.js | 116 +++ .../browser/browser_paste_resets_cut_highlights.js | 99 +++ .../tests/browser/browser_remove_bookmarks.js | 155 ++++ .../browser/browser_sidebar_bookmarks_telemetry.js | 141 ++++ .../browser/browser_sidebar_history_telemetry.js | 285 ++++++++ .../browser/browser_sidebar_open_bookmarks.js | 134 ++++ .../tests/browser/browser_sidebarpanels_click.js | 172 +++++ .../tests/browser/browser_sort_in_library.js | 248 +++++++ .../places/tests/browser/browser_stayopenmenu.js | 266 +++++++ .../browser/browser_toolbar_drop_bookmarklet.js | 200 ++++++ .../browser_toolbar_drop_multiple_flavors.js | 69 ++ ...owser_toolbar_drop_multiple_with_bookmarklet.js | 50 ++ .../tests/browser/browser_toolbar_drop_text.js | 142 ++++ .../browser/browser_toolbar_library_open_recent.js | 158 ++++ .../browser/browser_toolbar_other_bookmarks.js | 601 ++++++++++++++++ .../tests/browser/browser_toolbar_overflow.js | 436 +++++++++++ .../browser/browser_toolbarbutton_menu_context.js | 72 ++ .../browser_toolbarbutton_menu_show_in_folder.js | 92 +++ .../tests/browser/browser_views_iconsupdate.js | 132 ++++ .../tests/browser/browser_views_liveupdate.js | 493 +++++++++++++ .../places/tests/browser/favicon-normal16.png | Bin 0 -> 286 bytes .../components/places/tests/browser/frameLeft.html | 8 + .../places/tests/browser/frameRight.html | 8 + .../places/tests/browser/framedPage.html | 9 + browser/components/places/tests/browser/head.js | 533 ++++++++++++++ .../places/tests/browser/interactions/browser.ini | 30 + .../interactions/browser_interactions_blocklist.js | 108 +++ .../interactions/browser_interactions_referrer.js | 45 ++ .../interactions/browser_interactions_scrolling.js | 162 +++++ .../browser_interactions_scrolling_dom_history.js | 208 ++++++ .../interactions/browser_interactions_typing.js | 410 +++++++++++ .../browser_interactions_typing_dom_history.js | 171 +++++ .../interactions/browser_interactions_view_time.js | 398 ++++++++++ .../browser_interactions_view_time_dom_history.js | 123 ++++ .../places/tests/browser/interactions/head.js | 200 ++++++ .../tests/browser/interactions/scrolling.html | 121 ++++ .../browser/interactions/scrolling_subframe.html | 25 + .../places/tests/browser/keyword_form.html | 17 + .../places/tests/browser/pageopeningwindow.html | 11 + .../browser/sidebarpanels_click_test_page.html | 7 + browser/components/places/tests/chrome/chrome.ini | 10 + browser/components/places/tests/chrome/head.js | 36 + .../places/tests/chrome/test_0_bug510634.xhtml | 100 +++ ...t_bug1163447_selectItems_through_shortcut.xhtml | 88 +++ .../places/tests/chrome/test_bug549192.xhtml | 130 ++++ .../places/tests/chrome/test_bug549491.xhtml | 78 ++ .../chrome/test_selectItems_on_nested_tree.xhtml | 85 +++ .../places/tests/chrome/test_treeview_date.xhtml | 159 ++++ .../places/tests/unit/bookmarks.glue.html | 16 + .../places/tests/unit/bookmarks.glue.json | 83 +++ .../components/places/tests/unit/corruptDB.sqlite | Bin 0 -> 32772 bytes .../components/places/tests/unit/distribution.ini | 30 + .../components/places/tests/unit/head_bookmarks.js | 89 +++ .../tests/unit/test_PUIU_batchUpdatesForNode.js | 107 +++ .../tests/unit/test_PUIU_setCharsetForPage.js | 141 ++++ .../unit/test_PUIU_title_difference_spotter.js | 81 +++ .../tests/unit/test_browserGlue_bookmarkshtml.js | 33 + .../places/tests/unit/test_browserGlue_corrupt.js | 53 ++ .../unit/test_browserGlue_corrupt_nobackup.js | 47 ++ .../test_browserGlue_corrupt_nobackup_default.js | 50 ++ .../tests/unit/test_browserGlue_distribution.js | 143 ++++ .../places/tests/unit/test_browserGlue_migrate.js | 68 ++ .../places/tests/unit/test_browserGlue_prefs.js | 161 +++++ .../places/tests/unit/test_browserGlue_restore.js | 55 ++ .../tests/unit/test_clearHistory_shutdown.js | 183 +++++ .../tests/unit/test_interactions_blocklist.js | 67 ++ .../tests/unit/test_invalid_defaultLocation.js | 24 + browser/components/places/tests/unit/xpcshell.ini | 24 + 152 files changed, 21567 insertions(+) create mode 100644 browser/components/places/tests/browser/bookmark_dummy_1.html create mode 100644 browser/components/places/tests/browser/bookmark_dummy_2.html create mode 100644 browser/components/places/tests/browser/bookmarklet_windowOpen_dummy.html create mode 100644 browser/components/places/tests/browser/browser.ini create mode 100644 browser/components/places/tests/browser/browser_addBookmarkForFrame.js create mode 100644 browser/components/places/tests/browser/browser_autoshow_bookmarks_toolbar.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkMenu_hiddenWindow.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_addFolderDefaultButton.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_addKeywordForThisSearch.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_bookmarkAllTabs.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_cancel.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_editFolder.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_editTagContainer.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_newFolder.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_no_user_actions.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_readOnlyRoot.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_remember_folders.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_speculativeConnection.js create mode 100644 browser/components/places/tests/browser/browser_bookmarkProperties_xulStore.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_add_tags.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_all_tabs.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_backup_export_import.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_change_location.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_context_menu_contents.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_copy_folder_tree.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_folder_moveability.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_menu_ctrl_click.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_popup.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_private_window.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_remove_tags.js create mode 100644 browser/components/places/tests/browser/browser_bookmark_titles.js create mode 100644 browser/components/places/tests/browser/browser_bookmarklet_windowOpen.js create mode 100644 browser/components/places/tests/browser/browser_bookmarksProperties.js create mode 100644 browser/components/places/tests/browser/browser_bookmarks_change_title.js create mode 100644 browser/components/places/tests/browser/browser_bookmarks_change_url.js create mode 100644 browser/components/places/tests/browser/browser_bookmarks_sidebar_search.js create mode 100644 browser/components/places/tests/browser/browser_bookmarks_toolbar_context_menu_view_options.js create mode 100644 browser/components/places/tests/browser/browser_bookmarks_toolbar_telemetry.js create mode 100644 browser/components/places/tests/browser/browser_bug427633_no_newfolder_if_noip.js create mode 100644 browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js create mode 100644 browser/components/places/tests/browser/browser_bug631374_tags_selector_scroll.js create mode 100644 browser/components/places/tests/browser/browser_check_correct_controllers.js create mode 100644 browser/components/places/tests/browser/browser_click_bookmarks_on_toolbar.js create mode 100644 browser/components/places/tests/browser/browser_controller_onDrop.js create mode 100644 browser/components/places/tests/browser/browser_controller_onDrop_query.js create mode 100644 browser/components/places/tests/browser/browser_controller_onDrop_sidebar.js create mode 100644 browser/components/places/tests/browser/browser_controller_onDrop_tagFolder.js create mode 100644 browser/components/places/tests/browser/browser_copy_query_without_tree.js create mode 100644 browser/components/places/tests/browser/browser_cutting_bookmarks.js create mode 100644 browser/components/places/tests/browser/browser_default_bookmark_location.js create mode 100644 browser/components/places/tests/browser/browser_drag_bookmarks_on_toolbar.js create mode 100644 browser/components/places/tests/browser/browser_drag_folder_on_newTab.js create mode 100644 browser/components/places/tests/browser/browser_editBookmark_keywords.js create mode 100644 browser/components/places/tests/browser/browser_enable_toolbar_sidebar.js create mode 100644 browser/components/places/tests/browser/browser_forgetthissite.js create mode 100644 browser/components/places/tests/browser/browser_history_sidebar_search.js create mode 100644 browser/components/places/tests/browser/browser_import_button.js create mode 100644 browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js create mode 100644 browser/components/places/tests/browser/browser_library_bookmark_pages.js create mode 100644 browser/components/places/tests/browser/browser_library_bulk_tag_bookmarks.js create mode 100644 browser/components/places/tests/browser/browser_library_commands.js create mode 100644 browser/components/places/tests/browser/browser_library_delete.js create mode 100644 browser/components/places/tests/browser/browser_library_delete_bookmarks_in_tags.js create mode 100644 browser/components/places/tests/browser/browser_library_delete_tags.js create mode 100644 browser/components/places/tests/browser/browser_library_downloads.js create mode 100644 browser/components/places/tests/browser/browser_library_left_pane_middleclick.js create mode 100644 browser/components/places/tests/browser/browser_library_left_pane_select_hierarchy.js create mode 100644 browser/components/places/tests/browser/browser_library_middleclick.js create mode 100644 browser/components/places/tests/browser/browser_library_new_bookmark.js create mode 100644 browser/components/places/tests/browser/browser_library_openFlatContainer.js create mode 100644 browser/components/places/tests/browser/browser_library_open_all.js create mode 100644 browser/components/places/tests/browser/browser_library_open_all_with_separator.js create mode 100644 browser/components/places/tests/browser/browser_library_open_bookmark.js create mode 100644 browser/components/places/tests/browser/browser_library_open_leak.js create mode 100644 browser/components/places/tests/browser/browser_library_panel_leak.js create mode 100644 browser/components/places/tests/browser/browser_library_search.js create mode 100644 browser/components/places/tests/browser/browser_library_telemetry.js create mode 100644 browser/components/places/tests/browser/browser_library_tree_leak.js create mode 100644 browser/components/places/tests/browser/browser_library_views_liveupdate.js create mode 100644 browser/components/places/tests/browser/browser_library_warnOnOpen.js create mode 100644 browser/components/places/tests/browser/browser_markPageAsFollowedLink.js create mode 100644 browser/components/places/tests/browser/browser_panelview_bookmarks_delete.js create mode 100644 browser/components/places/tests/browser/browser_paste_bookmarks.js create mode 100644 browser/components/places/tests/browser/browser_paste_into_tags.js create mode 100644 browser/components/places/tests/browser/browser_paste_resets_cut_highlights.js create mode 100644 browser/components/places/tests/browser/browser_remove_bookmarks.js create mode 100644 browser/components/places/tests/browser/browser_sidebar_bookmarks_telemetry.js create mode 100644 browser/components/places/tests/browser/browser_sidebar_history_telemetry.js create mode 100644 browser/components/places/tests/browser/browser_sidebar_open_bookmarks.js create mode 100644 browser/components/places/tests/browser/browser_sidebarpanels_click.js create mode 100644 browser/components/places/tests/browser/browser_sort_in_library.js create mode 100644 browser/components/places/tests/browser/browser_stayopenmenu.js create mode 100644 browser/components/places/tests/browser/browser_toolbar_drop_bookmarklet.js create mode 100644 browser/components/places/tests/browser/browser_toolbar_drop_multiple_flavors.js create mode 100644 browser/components/places/tests/browser/browser_toolbar_drop_multiple_with_bookmarklet.js create mode 100644 browser/components/places/tests/browser/browser_toolbar_drop_text.js create mode 100644 browser/components/places/tests/browser/browser_toolbar_library_open_recent.js create mode 100644 browser/components/places/tests/browser/browser_toolbar_other_bookmarks.js create mode 100644 browser/components/places/tests/browser/browser_toolbar_overflow.js create mode 100644 browser/components/places/tests/browser/browser_toolbarbutton_menu_context.js create mode 100644 browser/components/places/tests/browser/browser_toolbarbutton_menu_show_in_folder.js create mode 100644 browser/components/places/tests/browser/browser_views_iconsupdate.js create mode 100644 browser/components/places/tests/browser/browser_views_liveupdate.js create mode 100644 browser/components/places/tests/browser/favicon-normal16.png create mode 100644 browser/components/places/tests/browser/frameLeft.html create mode 100644 browser/components/places/tests/browser/frameRight.html create mode 100644 browser/components/places/tests/browser/framedPage.html create mode 100644 browser/components/places/tests/browser/head.js create mode 100644 browser/components/places/tests/browser/interactions/browser.ini create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_referrer.js create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_scrolling.js create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_scrolling_dom_history.js create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_typing.js create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_typing_dom_history.js create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_view_time.js create mode 100644 browser/components/places/tests/browser/interactions/browser_interactions_view_time_dom_history.js create mode 100644 browser/components/places/tests/browser/interactions/head.js create mode 100644 browser/components/places/tests/browser/interactions/scrolling.html create mode 100644 browser/components/places/tests/browser/interactions/scrolling_subframe.html create mode 100644 browser/components/places/tests/browser/keyword_form.html create mode 100644 browser/components/places/tests/browser/pageopeningwindow.html create mode 100644 browser/components/places/tests/browser/sidebarpanels_click_test_page.html create mode 100644 browser/components/places/tests/chrome/chrome.ini create mode 100644 browser/components/places/tests/chrome/head.js create mode 100644 browser/components/places/tests/chrome/test_0_bug510634.xhtml create mode 100644 browser/components/places/tests/chrome/test_bug1163447_selectItems_through_shortcut.xhtml create mode 100644 browser/components/places/tests/chrome/test_bug549192.xhtml create mode 100644 browser/components/places/tests/chrome/test_bug549491.xhtml create mode 100644 browser/components/places/tests/chrome/test_selectItems_on_nested_tree.xhtml create mode 100644 browser/components/places/tests/chrome/test_treeview_date.xhtml create mode 100644 browser/components/places/tests/unit/bookmarks.glue.html create mode 100644 browser/components/places/tests/unit/bookmarks.glue.json create mode 100644 browser/components/places/tests/unit/corruptDB.sqlite create mode 100644 browser/components/places/tests/unit/distribution.ini create mode 100644 browser/components/places/tests/unit/head_bookmarks.js create mode 100644 browser/components/places/tests/unit/test_PUIU_batchUpdatesForNode.js create mode 100644 browser/components/places/tests/unit/test_PUIU_setCharsetForPage.js create mode 100644 browser/components/places/tests/unit/test_PUIU_title_difference_spotter.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_bookmarkshtml.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_corrupt.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_corrupt_nobackup_default.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_distribution.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_migrate.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_prefs.js create mode 100644 browser/components/places/tests/unit/test_browserGlue_restore.js create mode 100644 browser/components/places/tests/unit/test_clearHistory_shutdown.js create mode 100644 browser/components/places/tests/unit/test_interactions_blocklist.js create mode 100644 browser/components/places/tests/unit/test_invalid_defaultLocation.js create mode 100644 browser/components/places/tests/unit/xpcshell.ini (limited to 'browser/components/places/tests') 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 @@ + + +Bookmark Dummy 1 + + + +

Bookmark Dummy 1

+ + 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 @@ + + +Bookmark Dummy 2 + + + +

Bookmark Dummy 2

+ + 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 @@ + + +Bookmarklet windowOpen Dummy + + + +

Bookmarklet windowOpen Dummy

+ + diff --git a/browser/components/places/tests/browser/browser.ini b/browser/components/places/tests/browser/browser.ini new file mode 100644 index 0000000000..160b7ac09f --- /dev/null +++ b/browser/components/places/tests/browser/browser.ini @@ -0,0 +1,132 @@ +# 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_search.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_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..c40f0425cf --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarkProperties_folderSelection.js @@ -0,0 +1,170 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +const TEST_URL = "about:robots"; +let bookmarkPanel; +let folders; +let win; + +add_setup(async function () { + await PlacesUtils.bookmarks.eraseEverything(); + + Services.prefs.clearUserPref("browser.bookmarks.defaultLocation"); + + 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 iteract 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(); + }); +}); + +add_task(async function test_selectChoose() { + await clickBookmarkStar(win); + + // Open folder selector. + let menuList = win.document.getElementById("editBMPanel_folderMenuList"); + let folderTreeRow = win.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, {}, win); + await promisePopup; + + // Click the choose item. + EventUtils.synthesizeMouseAtCenter( + win.document.getElementById("editBMPanel_chooseFolderMenuItem"), + {}, + win + ); + + await TestUtils.waitForCondition( + () => !folderTreeRow.hidden, + "Should show the folder tree" + ); + let folderTree = win.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 = win.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( + win.document.getElementById("editBMPanel_foldersExpander"), + {}, + win + ); + + 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( + win.document.getElementById("editBMPanel_foldersExpander"), + {}, + win + ); + + await TestUtils.waitForCondition( + () => !folderTreeRow.hidden, + "Should re-show the folder tree" + ); + ok(input.hidden, "Folder tree should still not be broken."); + + await hideBookmarksPanel(win); + Assert.ok(!folderTree.view, "The view should have been disconnected"); +}); + +add_task(async function test_selectBookmarksMenu() { + await clickBookmarkStar(win); + + // Open folder selector. + let menuList = win.document.getElementById("editBMPanel_folderMenuList"); + + let promisePopup = BrowserTestUtils.waitForEvent( + menuList.menupopup, + "popupshown" + ); + EventUtils.synthesizeMouseAtCenter(menuList, {}, win); + await promisePopup; + + // Click the bookmarks menu item. + EventUtils.synthesizeMouseAtCenter( + win.document.getElementById("editBMPanel_bmRootItem"), + {}, + win + ); + + await TestUtils.waitForCondition( + () => + menuList.getAttribute("selectedGuid") == PlacesUtils.bookmarks.menuGuid, + "Should select the menu folder item" + ); + + Assert.equal( + menuList.label, + PlacesUtils.getString("BookmarksMenuFolderTitle"), + "Should have updated the menu label" + ); + + await hideBookmarksPanel(win); +}); 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..dc0151793a --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmark_add_tags.js @@ -0,0 +1,220 @@ +/* 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"; + + 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..7f656a33ee --- /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.is_visible(optionItem), + `Context menu option ${elementId} is visible` + ); +}; + +let OptionsMatchExpected = (contextMenu, expectedOptionItems) => { + let idList = []; + for (let elem of contextMenu.children) { + if ( + BrowserTestUtils.is_visible(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..b5385e3a08 --- /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,"; + +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", { accelKey: true }, 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", { accelKey: true }, 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..8b989b1958 --- /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,test%20data:%20url", + 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.loadURIString(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.loadURIString(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..c493a21a62 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bookmarksProperties.js @@ -0,0 +1,526 @@ +/* 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 } + ); + }); + + folderTree.addEventListener( + "DOMAttrModified", + function onDOMAttrModified(event) { + if (event.attrName != "place") { + return; + } + folderTree.removeEventListener("DOMAttrModified", onDOMAttrModified); + 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(); + }); + } + ); + 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..e11ae1cfc2 --- /dev/null +++ b/browser/components/places/tests/browser/browser_bug485100-change-case-loses-tag.js @@ -0,0 +1,74 @@ +"use strict"; + +const TEST_URL = "http://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, + }); + + 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..1c90667a19 --- /dev/null +++ b/browser/components/places/tests/browser/browser_check_correct_controllers.js @@ -0,0 +1,110 @@ +/* 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"); + ok(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"); + ok(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"); + ok(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..5ecad12a06 --- /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.is_visible(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..2fb932f300 --- /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 PlacesUtils.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..29dba4e5d0 --- /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 PlacesUtils.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..00a7e7ed39 --- /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 PlacesUtils.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..fe6211fd8e --- /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 PlacesUtils.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: sometext"], [...] ] + * 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..455b52a9c2 --- /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.mochi.test/first", + }, + { + title: "second", + url: "http://www.mochi.test/second", + }, + { + type: PlacesUtils.bookmarks.TYPE_SEPARATOR, + }, + { + title: "third", + url: "http://www.mochi.test/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, false, 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..0e94ce847f --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_bookmark_clear_visits.js @@ -0,0 +1,147 @@ +/* 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 showMostRecentColumn(library); + + 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 showMostRecentColumn(library); + + 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.loadURIString(gBrowser, url); + await onLoaded; + } +} + +async function showMostRecentColumn(library) { + const viewMenu = library.document.getElementById("viewMenu"); + const viewMenuPopup = library.document.getElementById("viewMenuPopup"); + const onViewMenuPopup = new Promise(resolve => { + viewMenuPopup.addEventListener("popupshown", () => resolve()); + }); + 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()); + }); + EventUtils.synthesizeMouseAtCenter(viewColumns, {}, library); + await onViewColumnsPopup; + + const mostRecentVisitColumnMenu = library.document.getElementById( + "menucol_placesContentDate" + ); + EventUtils.synthesizeMouseAtCenter(mostRecentVisitColumnMenu, {}, library); +} + +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..17c8b0948a --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_delete.js @@ -0,0 +1,116 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +/** + * Tests that Library correctly handles deletes. + */ + +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" + ); +}); 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_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_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..593b0bd243 --- /dev/null +++ b/browser/components/places/tests/browser/browser_library_views_liveupdate.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/. */ + +/** + * 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/`, + }); + item.title = `${prefix}1_edited`; + await updateAndCheckItem(item); + 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, + }); + 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/`, + }); + addedBookmarks.push(item); + + item = await insertAndCheckItem({ + parentGuid: folderGuid1, + title: `${prefix}f12`, + url: `http://${prefix}f12.mozilla.org/`, + }); + addedBookmarks.push(item); + + item.index = 0; + await updateAndCheckItem(item); + + 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]); + } +}); + +async function insertAndCheckItem(itemData, expectedIndex) { + let item = await PlacesUtils.bookmarks.insert(itemData); + + let [node, index, title] = getNodeForTreeItem( + item.guid, + gLibrary.PlacesOrganizer._places + ); + // Left pane should not be updated for normal bookmarks or separators. + switch (itemData.type || PlacesUtils.bookmarks.TYPE_BOOKMARK) { + case PlacesUtils.bookmarks.TYPE_BOOKMARK: + let uriString = itemData.url; + let isQuery = uriString.substr(0, 6) == "place:"; + if (isQuery) { + Assert.ok(node, "Should have a new query in the left pane."); + break; + } + // Fallthrough if this isn't a query + case PlacesUtils.bookmarks.TYPE_SEPARATOR: + Assert.ok( + !node, + "Should not have added a bookmark or separator to the left pane." + ); + break; + default: + Assert.ok( + node, + "Should have added a new node in the left pane for a folder." + ); + } + + if (node) { + Assert.equal(title, itemData.title, "Should have the correct title"); + Assert.equal(index, expectedIndex, "Should have the expected index"); + } + + return item; +} + +async function updateAndCheckItem(newItemData, expectedIndex) { + await PlacesUtils.bookmarks.update(newItemData); + + let [node, index, title] = getNodeForTreeItem( + newItemData.guid, + gLibrary.PlacesOrganizer._places + ); + + // Left pane should not be updated for normal bookmarks or separators. + switch (newItemData.type || PlacesUtils.bookmarks.TYPE_BOOKMARK) { + case PlacesUtils.bookmarks.TYPE_BOOKMARK: + let isQuery = newItemData.url.protocol == "place:"; + if (isQuery) { + Assert.ok(node, "Should be able to find the updated node"); + break; + } + // Fallthrough if this isn't a query + case PlacesUtils.bookmarks.TYPE_SEPARATOR: + Assert.ok(!node, "Should not be able to find the updated node"); + break; + default: + Assert.ok(node, "Should be able to find the updated node"); + } + + if (node) { + Assert.equal(title, newItemData.title, "Should have the correct title"); + Assert.equal(index, expectedIndex, "Should have the expected index"); + } +} + +async function removeAndCheckItem(itemData) { + await PlacesUtils.bookmarks.remove(itemData); + let [node] = getNodeForTreeItem( + itemData.guid, + gLibrary.PlacesOrganizer._places + ); + Assert.ok(!node, "Should not be able to find the removed node"); +} + +/** + * Get places node, index and cell text for a guid in a tree view. + * + * @param {string} aItemGuid + * item guid of the item to search. + * @param {object} aTree + * Tree to search in. + * @returns {Array} + * [node, index, cellText] or [null, null, ""] if not found. + */ +function getNodeForTreeItem(aItemGuid, aTree) { + function findNode(aContainerIndex) { + if (aTree.view.isContainerEmpty(aContainerIndex)) { + return [null, null, ""]; + } + + // 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 (let i = aContainerIndex + 1; i < aTree.view.rowCount; i++) { + let node = aTree.view.nodeForTreeIndex(i); + + if (node.bookmarkGuid == aItemGuid) { + // Minus one because we want relative index inside the container. + let tree = gLibrary.PlacesOrganizer._places; + let cellText = tree.view.getCellText(i, tree.columns.getColumnAt(0)); + return [node, i - aContainerIndex - 1, cellText]; + } + + if (PlacesUtils.nodeIsFolder(node)) { + // Open container. + aTree.view.toggleOpenState(i); + // Search inside it. + let foundNode = findNode(i); + // Close container. + aTree.view.toggleOpenState(i); + // Return node if found. + if (foundNode[0] != null) { + return foundNode; + } + } + + // We have finished walking this container. + if (!aTree.view.hasNextSibling(aContainerIndex + 1, i)) { + break; + } + } + return [null, null, ""]; + } + + // Root node is hidden, so we need to manually walk the first level. + for (let i = 0; i < aTree.view.rowCount; i++) { + // Open container. + aTree.view.toggleOpenState(i); + // Search inside it. + let foundNode = findNode(i); + // Close container. + aTree.view.toggleOpenState(i); + // Return node if found. + if (foundNode[0] != null) { + return foundNode; + } + } + return [null, null, ""]; +} 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..75ed89ddea --- /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 = PromiseUtils.defer(); + let deferredRightFrameVisit = PromiseUtils.defer(); + + 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..cec58bb305 --- /dev/null +++ b/browser/components/places/tests/browser/browser_paste_bookmarks.js @@ -0,0 +1,418 @@ +/* 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 bookmark = await PlacesUtils.bookmarks.insert({ + parentGuid: PlacesUtils.bookmarks.toolbarGuid, + url: TEST_URL, + title: "0", + }); + + 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"); + + 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..d159fa5646 --- /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); + ok(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_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__). +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..ee2eb8bbd4 --- /dev/null +++ b/browser/components/places/tests/browser/browser_stayopenmenu.js @@ -0,0 +1,266 @@ +/* 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.is_visible(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; + ok( + 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 `${title}`; + } + 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..3e3ff84b39 --- /dev/null +++ b/browser/components/places/tests/browser/browser_toolbar_drop_text.js @@ -0,0 +1,142 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ +// Instead of loading EventUtils.js into the test scope in browser-test.js for all tests, +// we only need EventUtils.js for a few files which is why we are using loadSubScript. +var EventUtils = {}; +Services.scriptloader.loadSubScript( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils +); + +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"); + ok( + 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..03d340b799 --- /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.${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.0/", + "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.${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 Binary files /dev/null and b/browser/components/places/tests/browser/favicon-normal16.png 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 @@ + + + Left frame + + + Open page in the right frame. + + 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 @@ + + + Right Frame + + + This is the right frame. + + 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 @@ + + + Framed page + + + + + + diff --git a/browser/components/places/tests/browser/head.js b/browser/components/places/tests/browser/head.js new file mode 100644 index 0000000000..1827d84cbc --- /dev/null +++ b/browser/components/places/tests/browser/head.js @@ -0,0 +1,533 @@ +ChromeUtils.defineESModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.sys.mjs", +}); + +XPCOMUtils.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; + // Simulate the click. + EventUtils.synthesizeMouse( + aTree.body, + x, + y, + aOptions || {}, + aTree.ownerGlobal + ); +} + +/** + * 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); +} + +registerCleanupFunction(async () => { + Services.prefs.clearUserPref("browser.bookmarks.defaultLocation"); + await PlacesTransactions.clearTransactionsHistory(true, true); +}); diff --git a/browser/components/places/tests/browser/interactions/browser.ini b/browser/components/places/tests/browser/interactions/browser.ini new file mode 100644 index 0000000000..79be009cbf --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser.ini @@ -0,0 +1,30 @@ +# 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] +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 +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure + +[browser_interactions_blocklist.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..c174024ef5 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_blocklist.js @@ -0,0 +1,108 @@ +/* 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, { + InteractionsBlocklist: "resource:///modules/InteractionsBlocklist.sys.mjs", +}); + +XPCOMUtils.defineLazyModuleGetters(this, { + FilterAdult: "resource://activity-stream/lib/FilterAdult.jsm", +}); + +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.loadURIString(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.loadURIString(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_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..925d427ff5 --- /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.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(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..e272f6c866 --- /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.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(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..6561a4d334 --- /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.loadURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: 0, + exactTypingTime: 0, + }, + ]); + + BrowserTestUtils.loadURIString(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.loadURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + keypresses: sentence.length, + typingTimeIsGreaterThan: 0, + }, + ]); + + BrowserTestUtils.loadURIString(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..7462a5080a --- /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.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(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..3bb76288eb --- /dev/null +++ b/browser/components/places/tests/browser/interactions/browser_interactions_view_time.js @@ -0,0 +1,398 @@ +/* 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.loadURIString(browser, TEST_URL2); + await BrowserTestUtils.browserLoaded(browser, false, TEST_URL2); + + await assertDatabaseValues([ + { + url: TEST_URL, + totalViewTime: 10000, + }, + ]); + + Interactions._pageViewStartTime = Cu.now() - 20000; + + BrowserTestUtils.loadURIString(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.loadURIString(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.loadURIString(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.loadURIString(otherWin.gBrowser.selectedBrowser, TEST_URL); + await BrowserTestUtils.browserLoaded( + otherWin.gBrowser.selectedBrowser, + false, + TEST_URL + ); + + Interactions._pageViewStartTime = Cu.now() - 10000; + + BrowserTestUtils.loadURIString(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.loadURIString(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.loadURIString( + 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..91aff9d100 --- /dev/null +++ b/browser/components/places/tests/browser/interactions/head.js @@ -0,0 +1,200 @@ +/* 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" +); + +const { sinon } = ChromeUtils.importESModule( + "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 @@ + + + + + + + + + +

Scrolling interaction tests

+ +


+ +
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas vitae condimentum erat, vel luctus nulla.
Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus.
Duis varius eleifend ex.
Vivamus tristique faucibus tortor, in commodo felis posuere eu.
Sed aliquam tristique enim at volutpat.
Phasellus a aliquam quam, ac hendrerit libero.
Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus.
Ut commodo lobortis egestas.
Vivamus vulputate nisi non ligula ullamcorper pulvinar.
Phasellus tincidunt leo et venenatis auctor.
Proin ac urna et risus ullamcorper viverra.
Morbi non nunc at augue tincidunt scelerisque.
Proin vel ligula erat.
Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non. + Donec mattis porttitor consectetur.
Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis.
Donec a mi mattis, feugiat lectus a, rutrum leo.
Suspendisse et tellus urna.
Quisque porta ex at efficitur venenatis.
Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed.
Pellentesque tristique tortor a nisl imperdiet convallis.
Curabitur ornare pharetra lacus at luctus.
+
+
+ +
+ +

+ + click to scroll to bottom + + +
+ + +









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+ +
+
Scroll inside me!
+
+ +

Middle

+ This is the middle anchor #1 + + click to scroll to top + +









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+









+ This is the bottom anchor #3 + + + 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 @@ + + + + + + + + + Subframe Content
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Maecenas vitae condimentum erat, vel luctus nulla.
Praesent nulla magna, rhoncus quis velit eu, dapibus efficitur lectus.
Duis varius eleifend ex.
Vivamus tristique faucibus tortor, in commodo felis posuere eu.
Sed aliquam tristique enim at volutpat.
Phasellus a aliquam quam, ac hendrerit libero.
Vivamus erat mi, mattis sit amet gravida id, malesuada ac lacus.
Ut commodo lobortis egestas.
Vivamus vulputate nisi non ligula ullamcorper pulvinar.
Phasellus tincidunt leo et venenatis auctor.
Proin ac urna et risus ullamcorper viverra.
Morbi non nunc at augue tincidunt scelerisque.
Proin vel ligula erat.
Aliquam dignissim pharetra leo, sit amet molestie sem efficitur non. + Donec mattis porttitor consectetur.
Nulla vulputate metus quis tellus pharetra, id maximus lacus mattis.
Donec a mi mattis, feugiat lectus a, rutrum leo.
Suspendisse et tellus urna.
Quisque porta ex at efficitur venenatis.
Sed volutpat malesuada mauris, sed dapibus dolor faucibus sed.
Pellentesque tristique tortor a nisl imperdiet convallis.
Curabitur ornare pharetra lacus at luctus.
+ + + 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 @@ + + + + + + + +
+ + +
+
+ + +
+ + diff --git a/browser/components/places/tests/browser/pageopeningwindow.html b/browser/components/places/tests/browser/pageopeningwindow.html new file mode 100644 index 0000000000..e98c7242ab --- /dev/null +++ b/browser/components/places/tests/browser/pageopeningwindow.html @@ -0,0 +1,11 @@ + +Hi, I was opened via a
+ 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 @@ + + + browser_sidebarpanels_click.js test page + + + + diff --git a/browser/components/places/tests/chrome/chrome.ini b/browser/components/places/tests/chrome/chrome.ini new file mode 100644 index 0000000000..3ca7ebdd60 --- /dev/null +++ b/browser/components/places/tests/chrome/chrome.ini @@ -0,0 +1,10 @@ +[DEFAULT] +support-files = head.js + +[test_0_bug510634.xhtml] +[test_bug1163447_selectItems_through_shortcut.xhtml] +skip-if = (os == 'win' && processor == 'aarch64') # bug 1532775 +[test_bug549192.xhtml] +[test_bug549491.xhtml] +[test_selectItems_on_nested_tree.xhtml] +[test_treeview_date.xhtml] \ No newline at end of file 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 @@ + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + 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 @@ + + + +Bookmarks +

Bookmarks Menu

+ +

+

example +

Bookmarks Toolbar

+
Add bookmarks to this folder to see them displayed on the Bookmarks Toolbar +

+

example +

+

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 Binary files /dev/null and b/browser/components/places/tests/unit/corruptDB.sqlite 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..8aabfb3832 --- /dev/null +++ b/browser/components/places/tests/unit/head_bookmarks.js @@ -0,0 +1,89 @@ +/* -*- 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; + +function checkItemHasAnnotation(guid, name) { + return PlacesUtils.promiseItemId(guid).then(id => { + let hasAnnotation = PlacesUtils.annotations.itemHasAnnotation(id, name); + Assert.ok(hasAnnotation, `Expected annotation ${name}`); + }); +} + +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..e2bee06bcb --- /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.import( + "resource:///modules/distribution.js" + ); + 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.ini b/browser/components/places/tests/unit/xpcshell.ini new file mode 100644 index 0000000000..4c0d0f7c92 --- /dev/null +++ b/browser/components/places/tests/unit/xpcshell.ini @@ -0,0 +1,24 @@ +[DEFAULT] +head = head_bookmarks.js +firefox-appdir = browser +skip-if = toolkit == 'android' # bug 1730213 +support-files = + bookmarks.glue.html + bookmarks.glue.json + corruptDB.sqlite + distribution.ini + +[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] +[test_PUIU_batchUpdatesForNode.js] +[test_PUIU_setCharsetForPage.js] +[test_PUIU_title_difference_spotter.js] -- cgit v1.2.3